Merge remote-tracking branch 'origin/field/ptop-1st' into field/ptop-1st
This commit is contained in:
commit
d5fc47c549
577
docs/분석/2026/06/20/191351_recipe_page_통합신호_핸드셰이크_재구성_분석.md
Normal file
577
docs/분석/2026/06/20/191351_recipe_page_통합신호_핸드셰이크_재구성_분석.md
Normal file
@ -0,0 +1,577 @@
|
|||||||
|
# SeqAutoRunPTOP — Recipe/Page 신호 "동시 수신" 대응 핸드셰이크 재구성 분석
|
||||||
|
|
||||||
|
- 작성일: 2026-06-20
|
||||||
|
- 대상: `Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs`
|
||||||
|
- 분류: **사전 분석 (소스 미수정)** — 코드 프리뷰만 포함
|
||||||
|
- **결론은 8장(현장 확정안: 단일 PAGE 게이트 핸드셰이크).** 3~4장 Option 1/2는 검토 이력.
|
||||||
|
- 관련 문서: `132647_uframe_가접_recipe_추가_seqautorun_분석.md`, `133714_recipe전환시_scout필드_이전레시피_잔존_분석.md`
|
||||||
|
- 관련 메모: PLC DeviceMap 260617 pending, PLC/IO 체크 260620
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 배경 (현장 확인, 2026-06-20)
|
||||||
|
|
||||||
|
- PLC/IO 체크 완료.
|
||||||
|
- **현장 PLC는 Recipe 변경 신호와 Page 변경 신호를 따로따로 주지 않고 "동시에" 준다.**
|
||||||
|
- 즉 `PLC_RECIPE_CHANGE_REQ`(0x09)와 `PLC_PAGE_CHANGE_REQ`(0x08)를 같은 타이밍에 ON 하고,
|
||||||
|
`PLC_RECIPE_NO`(D idx2)와 `PLC_PAGE_NO`(D idx1) 워드도 한 번에 실어 보낸다.
|
||||||
|
- 그런데 현재 SW는 **Recipe 핸드셰이크와 Page 핸드셰이크를 별개의 독립 노드**로 처리한다(아래 2장).
|
||||||
|
- 추가로 **U-Frame 가접(tack weld)** 이 곧 들어온다 → 본 용접 전 선행 발진 패스 필요(Case B 유력, 별도 문서 참조).
|
||||||
|
|
||||||
|
> 본 문서는 "신호가 동시에 오는 환경에 맞춰 SeqAutoRunPTOP의 핸드셰이크를 어떻게 재구성할지"를
|
||||||
|
> 소스 수정 없이 설계하고, 적용 시 들어갈 코드를 미리 보여주는 것이 목적이다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 현재 구조 (문제점)
|
||||||
|
|
||||||
|
### 2-1. 노드 4개 — Recipe와 Page가 비대칭
|
||||||
|
|
||||||
|
| 노드 | 종류 | 동작 | REQ 대기 방식 |
|
||||||
|
|------|------|------|----------------|
|
||||||
|
| `CheckRecipeNo` (라인 298) | Decision | `PLC_RECIPE_CHANGE_REQ` **1회 peek** | **논블로킹** (OFF면 즉시 Page로) |
|
||||||
|
| `CheckRecipeNoComp` (라인 319) | Action | RecipeNo 3회 읽기 → 활성화 → 에코/완료비트 | — |
|
||||||
|
| `CheckPageNo` (라인 396) | Action | `PLC_PAGE_CHANGE_REQ` ON 대기 | **블로킹** (무한 대기) |
|
||||||
|
| `CheckPageNoComp` (라인 450) | Action | PageNo 3회 읽기 → 검증 → 에코/완료비트 | — |
|
||||||
|
|
||||||
|
Flow (라인 153~177):
|
||||||
|
|
||||||
|
```
|
||||||
|
CheckInitCondition
|
||||||
|
→ CheckRecipeNo ──(REQ ON)──→ CheckRecipeNoComp ┐
|
||||||
|
└────────(REQ OFF, 논블로킹)───────────────┴→ CheckPageNo → CheckPageNoComp → ...
|
||||||
|
CheckCycleStop ──루프백──→ CheckRecipeNo (매 사이클 Recipe부터)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2-2. 동시 수신 환경에서 무엇이 문제인가
|
||||||
|
|
||||||
|
현재 설계의 **암묵적 가정**은 "Recipe 변경은 가끔, Page 변경은 매 사이클" 이라
|
||||||
|
→ Recipe는 **논블로킹 peek**, Page는 **블로킹 대기** 로 둔 것이다.
|
||||||
|
|
||||||
|
PLC가 둘을 **동시에** 주면 다음 리스크가 생긴다:
|
||||||
|
|
||||||
|
1. **완료 응답이 분리됨**
|
||||||
|
`PC_RECIPE_CHANGE_COMPLETE`와 `PC_PAGE_CHANGE_COMPLETE`가 서로 다른 노드에서, 시간차(레시피 활성화 폴링 수백 ms~초)를 두고 set 된다.
|
||||||
|
PLC가 "두 완료를 한 묶음으로" 기다린다면 핸드셰이크 타이밍이 어긋난다.
|
||||||
|
2. **REQ 드롭 타이밍 race**
|
||||||
|
PLC가 첫 COMPLETE를 보고 **두 REQ를 동시에 내리면**, 아직 처리 안 한 다른 REQ를 SW가 놓칠 수 있다.
|
||||||
|
특히 `CheckRecipeNo`는 1회 peek이라, Page 처리 도중 Recipe REQ가 내려가면 다음 사이클 판정이 흔들린다.
|
||||||
|
3. **검증 3회 × 2종 = 직렬 지연**
|
||||||
|
RecipeNo 3회(600ms) → 활성화 폴링 → PageNo 3회(600ms)로 직렬 누적. 동시 수신이면 한 번에 스냅샷 떠서 병렬 검증이 맞다.
|
||||||
|
4. **순서 의존성은 유지 필요**
|
||||||
|
Page 검증은 `_ScannerCanvasUserCtrl.Model.Layers.Count()`(= 활성 레시피 페이지 수)에 의존(라인 490).
|
||||||
|
따라서 **"Recipe 활성화 → 그 다음 Page 검증"** 순서는 반드시 지켜야 한다. (동시 수신이어도 처리 순서는 Recipe 먼저)
|
||||||
|
|
||||||
|
> ⚠️ 추가 확인 포인트(코드 현상태): 완료비트를 **`false`로 write** 한다(라인 387, 529).
|
||||||
|
> "결정 3 미러형" 주석이 붙어있고 기존 Page 핸드셰이크와 동일 관례지만,
|
||||||
|
> **IO 체크까지 끝난 지금 PLC와 COMPLETE 비트 극성(active-high/low)을 반드시 재확인**할 것. (5장 체크리스트)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 재구성 설계 — 통합 핸드셰이크
|
||||||
|
|
||||||
|
핵심 아이디어: **"하나의 변경 요청"으로 보고, REQ 대기 1 노드 + Recipe·Page 동시 처리 1 노드로 합친다.**
|
||||||
|
|
||||||
|
```
|
||||||
|
변경 전: CheckRecipeNo(peek) → CheckRecipeNoComp → CheckPageNo(wait) → CheckPageNoComp
|
||||||
|
변경 후: CheckChangeReq(wait) → ApplyRecipeAndPage (Recipe 먼저 활성화 → Page 검증/에코 → 두 COMPLETE 동시)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3-A. [권장] Option 1 — 노드 통합 (2노드)
|
||||||
|
|
||||||
|
- `CheckChangeReq` : **PAGE_REQ 또는 RECIPE_REQ 중 하나라도 ON** 될 때까지 블로킹 대기(동시 수신이므로 사실상 같이 ON). REQ 상태를 래치.
|
||||||
|
- `ApplyRecipeAndPage` : 두 워드 스냅샷 → (RECIPE_REQ면) 레시피 활성화 → Page 검증 → 에코 → **두 COMPLETE 동시 set**.
|
||||||
|
- 장점: 완료비트 동시 처리/REQ 동시 소비가 자연스러움. 직렬 지연 제거. 의도가 코드에 드러남.
|
||||||
|
- 단점: 노드 통합이라 diff가 큼(아래 프리뷰로 사전 검토).
|
||||||
|
|
||||||
|
### 3-B. Option 2 — 최소 변경 (기존 4노드 유지, 대기 조건만 통합)
|
||||||
|
|
||||||
|
- `CheckRecipeNo`를 논블로킹 peek → **블로킹 대기**로 바꾸고, 대기 조건을 `(RECIPE_REQ || PAGE_REQ)` 로.
|
||||||
|
- `CheckPageNo`의 블로킹 대기는 제거(이미 위에서 함께 대기했으므로 통과).
|
||||||
|
- 완료비트는 여전히 분리 노드에서 set → **2번 리스크(완료 동시성)는 부분적으로만 해소**.
|
||||||
|
- 장점: diff 최소. 단점: 완료비트 동시성/REQ 동시 소비가 깔끔히 안 됨 → 현장 PLC가 "두 완료 동시"를 요구하면 부족.
|
||||||
|
|
||||||
|
> **권장: Option 1.** 현장이 "동시에 준다 + 동시에 받길 원한다"면 완료비트도 한 노드에서 동시에 내보내는 통합이 정석.
|
||||||
|
> 단 PLC가 "REQ는 동시지만 완료는 각각 받아도 된다"면 Option 2로도 충분(5장에서 확정).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 코드 프리뷰 (Option 1 적용 시) — *아직 미적용*
|
||||||
|
|
||||||
|
### 4-1. 노드 상수 / Builder 등록 / Flow
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ── 상수: 기존 4개(RECIPENO, RECIPENO_COMPLETE, PAGENO, PAGENO_COMPLETE)를 2개로 대체 ──
|
||||||
|
const string SEQ_CHECK_CHANGE_REQ = "[SeqAutoRunPTOP] CHECK Change Req (Recipe+Page)";
|
||||||
|
const string SEQ_APPLY_RECIPE_AND_PAGE = "[SeqAutoRunPTOP] Apply Recipe & Page";
|
||||||
|
|
||||||
|
// ── Builder() 등록 ──
|
||||||
|
sb.AddAction(SEQ_CHECK_CHANGE_REQ, CheckChangeReq); // REQ(둘 중 하나라도) ON 대기 + 래치
|
||||||
|
sb.AddAction(SEQ_APPLY_RECIPE_AND_PAGE, ApplyRecipeAndPage); // Recipe 활성 → Page 검증 → 에코 → 두 COMPLETE 동시
|
||||||
|
|
||||||
|
// ── Flow (메인) ──
|
||||||
|
sb.Flows(sb.Init
|
||||||
|
> sb[SEQ_ENTER]
|
||||||
|
> sb[SEQ_CHECK_INIT_CONDITION]
|
||||||
|
> sb[SEQ_CHECK_CHANGE_REQ] // ← Recipe/Page 통합 대기
|
||||||
|
> sb[SEQ_APPLY_RECIPE_AND_PAGE] // ← Recipe 먼저, 그 다음 Page (순서 보존)
|
||||||
|
> sb[SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ]
|
||||||
|
> sb[SEQ_DO_ID_UPLOAD_WELDING_LIST_SEQ]
|
||||||
|
> sb[SEQ_GET_RESULT_UPLOAD_WELDING_LIST_SEQ]
|
||||||
|
> sb[SEQ_CHECK_WELDING_START_READ_BIT]
|
||||||
|
> sb[SEQ_WAIT_WELDING_START]
|
||||||
|
> sb[SEQ_EXECUTE_WELDING]
|
||||||
|
> sb[SEQ_CHECK_CYCLE_STOP]
|
||||||
|
> sb[SEQ_SAVE_RESULT_DATA]
|
||||||
|
> sb[SEQ_WRITE_COMPLETION_PLC_BIT]
|
||||||
|
> sb[SEQ_COMPLETE]
|
||||||
|
> sb.Final);
|
||||||
|
|
||||||
|
// ── Flow (루프백/예외) ──
|
||||||
|
sb.Flows(sb[SEQ_CHECK_CYCLE_STOP] > sb[SEQ_CHECK_CHANGE_REQ]); // 매 사이클 통합 대기부터
|
||||||
|
sb.Flows(sb[SEQ_CHECK_INIT_CONDITION] > sb[SEQ_COMPLETE] > sb.Final);
|
||||||
|
// (기존의 CheckRecipeNo→CheckPageNo "REQ OFF 논블로킹 우회" Flow 라인은 삭제)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4-2. `CheckChangeReq` — 통합 대기 노드 (CheckPageNo 패턴 재사용)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 동시 수신 전제: 둘 중 하나라도 ON 되면 같은 스캔에 둘 다 ON 되어 있다고 보고 진입.
|
||||||
|
// (혹시 미세 시간차가 있어도, ApplyRecipeAndPage 진입 시 다시 두 REQ를 함께 래치하므로 안전)
|
||||||
|
private bool _reqRecipe; // 이번 사이클 Recipe 변경 요청 동반 여부
|
||||||
|
private bool _reqPage;
|
||||||
|
|
||||||
|
private SequenceActionResult CheckChangeReq(SequenceContext context, string id, SequenceActionResult param)
|
||||||
|
{
|
||||||
|
Log.I("[CheckChangeReq] PLC PAGE/RECIPE CHANGE REQ 대기 중...");
|
||||||
|
Status.SetWorkingPosition(eWorkingPosition.LOADING_CELL);
|
||||||
|
|
||||||
|
var t0 = DateTime.Now;
|
||||||
|
while (!context.Stopped)
|
||||||
|
{
|
||||||
|
if (_PLC == null) { Log.E("[CheckChangeReq] _PLC null"); return null; }
|
||||||
|
|
||||||
|
bool page = _PLC.ReadBit(eBitInput.PLC_PAGE_CHANGE_REQ).data;
|
||||||
|
bool recipe = _PLC.ReadBit(eBitInput.PLC_RECIPE_CHANGE_REQ).data;
|
||||||
|
|
||||||
|
if (page || recipe)
|
||||||
|
{
|
||||||
|
_reqPage = page;
|
||||||
|
_reqRecipe = recipe;
|
||||||
|
Log.I($"---> [CheckChangeReq] REQ ON 감지 (PAGE={page}, RECIPE={recipe}) → ApplyRecipeAndPage 이동");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.Now.Subtract(t0).TotalSeconds > PLC_TIMEOUT) {
|
||||||
|
Log.W("[CheckChangeReq] REQ 대기 TIMEOUT...");
|
||||||
|
t0 = DateTime.Now;
|
||||||
|
}
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
|
if (context.Stopped) return null;
|
||||||
|
return SequenceAction.Ok();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> ※ "Page는 매 사이클 필수, Recipe는 가끔"이 현장에서도 유지된다면 대기 게이트를 `page` 단독으로 두고
|
||||||
|
> 그 시점의 `recipe`를 함께 래치해도 된다(동시 수신이므로 결과 동일). 5장에서 확정.
|
||||||
|
|
||||||
|
### 4-3. `ApplyRecipeAndPage` — Recipe 활성 → Page 검증 → 에코 → 두 COMPLETE 동시
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 기존 CheckRecipeNoComp + CheckPageNoComp 를 한 노드로 합침.
|
||||||
|
// 순서 불변식: ① 두 워드 스냅샷 → ② (요청 시) 레시피 활성화 → ③ 활성 레시피 기준 Page 검증
|
||||||
|
// → ④ 두 워드 에코 → ⑤ 두 COMPLETE "동시" set
|
||||||
|
private SequenceActionResult ApplyRecipeAndPage(SequenceContext context, string id, SequenceActionResult param)
|
||||||
|
{
|
||||||
|
// ── ① 워드 스냅샷 (3회 검증) ─ PLC가 동시에 실어보낸 값 ──
|
||||||
|
plcRecipeNo = ReadWordVerified(eWordInput.PLC_RECIPE_NO, 3);
|
||||||
|
plcPageNo = ReadWordVerified(eWordInput.PLC_PAGE_NO, 3);
|
||||||
|
if (context.Stopped) return null;
|
||||||
|
_CurrentPageNo = plcPageNo - 1;
|
||||||
|
|
||||||
|
string cellId = _PLC.CellId();
|
||||||
|
string lotId = _PLC.LotId();
|
||||||
|
Status.GetProcessingWeldingData().CellId = cellId;
|
||||||
|
Status.GetProcessingWeldingData().LotId = lotId;
|
||||||
|
Log.I($"[ApplyRecipeAndPage] snapshot Recipe={plcRecipeNo}, Page={plcPageNo}, Cell={cellId}, Lot={lotId}");
|
||||||
|
|
||||||
|
// ── ② 레시피 활성화 (요청 동반 시 + 실제로 바뀐 경우만) ──
|
||||||
|
if (_reqRecipe)
|
||||||
|
{
|
||||||
|
var recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == plcRecipeNo);
|
||||||
|
if (recipe == null) {
|
||||||
|
Log.E($"[ApplyRecipeAndPage] {plcRecipeNo}번 레시피 없음");
|
||||||
|
MessageUtil.ShowErrorToast($"PLC Recipe No [{plcRecipeNo}] 레시피 없음");
|
||||||
|
Thread.Sleep(3000);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var pre = _RecipeManager.ActivatedRecipe();
|
||||||
|
if (pre == null || pre.Id != recipe.Id) {
|
||||||
|
_RecipeManager.ActivateRecipe(recipe.Id, "PLC Recipe Change");
|
||||||
|
var aS = DateTime.Now;
|
||||||
|
while (!context.Stopped) { // 반영 폴링
|
||||||
|
var cur = _RecipeManager.ActivatedRecipe();
|
||||||
|
if (cur != null && cur.Id == recipe.Id) break;
|
||||||
|
if (DateTime.Now.Subtract(aS).TotalSeconds > PLC_TIMEOUT) { Log.W("[ApplyRecipeAndPage] 활성 반영 TIMEOUT"); aS = DateTime.Now; }
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
|
if (context.Stopped) return null;
|
||||||
|
Log.I($"[ApplyRecipeAndPage] ActivateRecipe → {recipe.Id}");
|
||||||
|
} else {
|
||||||
|
Log.I($"[ApplyRecipeAndPage] 이미 활성({recipe.Id}) → 재활성 생략");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ③ Page 검증 (★ 반드시 레시피 활성화 "후" — 레이어 수가 레시피에 의존) ──
|
||||||
|
int recipePageCount = _ScannerCanvasUserCtrl.Model.Layers.Count();
|
||||||
|
if (plcPageNo < 1 || plcPageNo > recipePageCount) {
|
||||||
|
Log.E($"[ApplyRecipeAndPage] Invalid PageNo {plcPageNo} (활성 레시피 페이지 수 {recipePageCount})");
|
||||||
|
MessageUtil.ShowErrorToast($"Invalid PageNo {plcPageNo}");
|
||||||
|
Thread.Sleep(3000);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ④ 에코 ──
|
||||||
|
_PLC.WriteWord(eWordOutput.PC_RECIPE_NO, plcRecipeNo);
|
||||||
|
_PLC.WriteWord(eWordOutput.PC_PAGE_NO, plcPageNo);
|
||||||
|
|
||||||
|
// ── ⑤ 두 COMPLETE "동시" set (※ 극성은 5장에서 PLC와 확정 — 현재 코드 관례는 false) ──
|
||||||
|
_PLC.WriteBit(eBitOutput.PC_RECIPE_CHANGE_COMPLETE, false);
|
||||||
|
_PLC.WriteBit(eBitOutput.PC_PAGE_CHANGE_COMPLETE, false);
|
||||||
|
Log.I($"[ApplyRecipeAndPage] echo+complete (Recipe={plcRecipeNo}, Page={plcPageNo}) → Upload 이동");
|
||||||
|
|
||||||
|
// UI 페이지 갱신
|
||||||
|
Application.Current.AuroraEnv().EventPublisher.Publish(
|
||||||
|
new EventMessage("", CommonEventIds.EVENT_AUTO_RUN_UPDATE_PAGENUM, (int)_CurrentPageNo));
|
||||||
|
|
||||||
|
Thread.Sleep(5);
|
||||||
|
return SequenceAction.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3회 연속 읽기 헬퍼 (기존 CheckRecipeNoComp/CheckPageNoComp의 중복 루프를 공통화)
|
||||||
|
private short ReadWordVerified(eWordInput addr, int times)
|
||||||
|
{
|
||||||
|
short last = 0;
|
||||||
|
var buf = new List<short>();
|
||||||
|
for (int i = 0; i < times; i++) {
|
||||||
|
last = _PLC.ReadWord(addr).data;
|
||||||
|
buf.Add(last);
|
||||||
|
Thread.Sleep(200);
|
||||||
|
}
|
||||||
|
Log.I($"[ReadWordVerified] {addr} = [{string.Join("/", buf)}]");
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 삭제 대상(통합으로 흡수): `CheckRecipeNo`, `CheckRecipeNoComp`, `CheckPageNo`, `CheckPageNoComp` 및
|
||||||
|
> 관련 상수 4개·Flow 2줄. `plcRecipeNo`/`plcPageNo`/`_CurrentPageNo` 필드는 그대로 재사용.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 가접(tack weld) 연계 — ExecuteWelding (Case B 프리뷰)
|
||||||
|
|
||||||
|
핸드셰이크 통합과 **별개 축**이지만 같이 들어올 작업이므로 함께 설계한다.
|
||||||
|
어제 분석 결론(별도 문서)대로 가접은 **본접 전 선행 패스**(Case B)가 유력.
|
||||||
|
|
||||||
|
핸드셰이크 통합 후에도 가접은 `ExecuteWelding`(라인 720) 한 곳에만 닿는다. 두 가지 구현 갈래:
|
||||||
|
|
||||||
|
- **Case A (데이터 구동, 코드 무수정)**: 가접 Dot/짧은 Line을 티칭에서 본접 **앞 순서 오브젝트**로 넣어 한 리스트로 연속 발진. → `ExecuteWelding` 손 안 댐.
|
||||||
|
- **Case B (별도 선행 패스, 코드 수정)**: 가접 전용 리스트(예: `eListNo.LIST2`)를 본접 전에 먼저 실행.
|
||||||
|
|
||||||
|
Case B 적용 시 `ExecuteWelding` 골격(프리뷰):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 가접 리스트가 구성돼 있으면 본접 전에 선행 발진
|
||||||
|
if (_TackScannerListNo != eListNo.NONE) // 신규 필드 (없으면 스킵 → Case A와 호환)
|
||||||
|
{
|
||||||
|
GateOn(); // Core+Ring Gate ON (기존 두 줄 래핑)
|
||||||
|
Thread.Sleep(laserEmissionGateDelayMs);
|
||||||
|
_Scanner.ExecuteList(_TackScannerListNo); // 가접 발진
|
||||||
|
_Scanner.WaitForListTaskCompleted(1000);
|
||||||
|
GateOff();
|
||||||
|
Log.I("[ExecuteWelding] Tack(가접) pass 완료 → 본접 진행");
|
||||||
|
// (가접/본접 Gate를 분리할지 한 Gate로 묶을지는 파라미터 동일 여부에 따라 결정)
|
||||||
|
}
|
||||||
|
|
||||||
|
GateOn(); // 본접
|
||||||
|
Thread.Sleep(laserEmissionGateDelayMs);
|
||||||
|
_Scanner.ExecuteList(_WeldingScannerListNo);
|
||||||
|
_Scanner.WaitForListTaskCompleted(1000);
|
||||||
|
GateOff();
|
||||||
|
```
|
||||||
|
|
||||||
|
수반 작업: ① `eListNo`에 가접 리스트 추가 ② 업로드 단계(`SetUploadWeldingList_SeqParams`)에서 가접 리스트도 업로드
|
||||||
|
③ TactTime/`_WeldingSpanTime` 측정 구간 재정의 ④ 가접 파라미터가 다르면 별도 Gate/타이밍.
|
||||||
|
또는 노드 자체를 `SEQ_EXECUTE_TACK` → `SEQ_EXECUTE_WELDING` 으로 분리해 Builder Flow에 한 단계 추가.
|
||||||
|
|
||||||
|
> 상세는 `132647_uframe_가접_recipe_추가_seqautorun_분석.md` 참조. (본 문서는 통합 핸드셰이크와의 정합만 확인)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 의사결정 체크리스트 (PLC/현장 확정 필요)
|
||||||
|
|
||||||
|
- [ ] PLC는 변경 시 **단일 통합 REQ 비트**를 쓰는가, 아니면 `PAGE_REQ`+`RECIPE_REQ` **두 비트를 동시 ON** 하는가?
|
||||||
|
- [ ] **완료 응답**: 두 COMPLETE를 **동시에** 받길 원하나(→ Option 1), 각각 받아도 되나(→ Option 2)?
|
||||||
|
- [ ] **COMPLETE 비트 극성**: 현재 코드는 `false` write(라인 387/529). PLC 기대값(active-high/low) 재확인. ← IO 체크 끝났으니 지금 확정 가능.
|
||||||
|
- [ ] REQ 드롭 시점: PLC가 COMPLETE 보고 REQ를 내리나? (handshake 종결 조건)
|
||||||
|
- [ ] 매 사이클 Page는 항상 REQ를 주나(블로킹 게이트로 적합한지)?
|
||||||
|
- [ ] 가접: Case A(리스트 합치기) vs Case B(별도 패스)? 가접 파라미터가 본접과 다른가?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 한 줄 요약
|
||||||
|
|
||||||
|
> 현장이 Recipe/Page 신호를 **동시에** 주므로, 비대칭(peek vs 블로킹)인 현재 4노드를
|
||||||
|
> **단일 PAGE 게이트 핸드셰이크 1노드**로 합친다(8장 = 현장 확정안). PAGE_REQ 하나만 게이트로 쓰되,
|
||||||
|
> 그 안에서 **RecipeNum으로 레시피를 실제 활성화 → 그 활성 레시피의 PageNum 레이어로 용접리스트 생성**(순서 불변식)하고,
|
||||||
|
> RecipeNum/PageNum을 PC→PLC에 에코한 뒤 **PAGE COMPLETE 하나만** 응답한다. (3~4장 Option 1/2는 검토 이력)
|
||||||
|
> **본 문서는 분석/프리뷰이며 소스는 수정하지 않았다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. ★ 현장 확정안 (2026-06-20 갱신) — 단일 PAGE 게이트 핸드셰이크
|
||||||
|
|
||||||
|
> 현장 결정: **RECIPE_REQ 비트는 보지 않는다.** PAGE 신호 하나만 게이트로 쓰고,
|
||||||
|
> 그 안에서 RecipeNum·PageNum 워드를 같이 읽어 PC→PLC에 에코한 뒤, **PAGE COMPLETE만** 보낸다.
|
||||||
|
> (Recipe 전용 REQ/COMPLETE 핸드셰이크는 폐기 — PLC가 동시에 주므로 PAGE 신호가 둘을 대표)
|
||||||
|
|
||||||
|
### 8-1. 동작 순서
|
||||||
|
|
||||||
|
```
|
||||||
|
1) PLC_PAGE_CHANGE_REQ(0x08) ON 대기 ← 단일 게이트 (RECIPE_REQ는 미감시)
|
||||||
|
2) PLC_RECIPE_NO(D idx2), PLC_PAGE_NO(D idx1) 워드 읽기
|
||||||
|
3) (레시피 활성화 — 8-3 확인 포인트)
|
||||||
|
4) 에코: PC_RECIPE_NO ← 읽은 RecipeNum, PC_PAGE_NO ← 읽은 PageNum (PC→PLC 주소에 그대로 적기)
|
||||||
|
5) PC_PAGE_CHANGE_COMPLETE(0x08) 송신 ← 페이지 COMP 신호 하나만
|
||||||
|
```
|
||||||
|
|
||||||
|
기존 4노드(`CheckRecipeNo`/`CheckRecipeNoComp`/`CheckPageNo`/`CheckPageNoComp`)를
|
||||||
|
**`CheckPageHandshake` 1노드**로 합친다(또는 대기/처리 2노드로 분리해도 무방).
|
||||||
|
|
||||||
|
### 8-2. 코드 프리뷰 (확정안) — *미적용*
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Builder 등록: 기존 4개 노드/상수 → 1개로 대체
|
||||||
|
const string SEQ_CHECK_PAGE_HANDSHAKE = "[SeqAutoRunPTOP] Check Page Handshake (Recipe+Page echo)";
|
||||||
|
sb.AddAction(SEQ_CHECK_PAGE_HANDSHAKE, CheckPageHandshake);
|
||||||
|
|
||||||
|
// Flow: ... > CheckInitCondition > CheckPageHandshake > SetUploadWeldingList ...
|
||||||
|
// 루프백: CheckCycleStop > CheckPageHandshake
|
||||||
|
// (CheckRecipeNo→CheckPageNo 우회 Flow 등 Recipe 관련 Flow 2줄 삭제)
|
||||||
|
|
||||||
|
private SequenceActionResult CheckPageHandshake(SequenceContext context, string id, SequenceActionResult param)
|
||||||
|
{
|
||||||
|
Log.I("[CheckPageHandshake] PLC_PAGE_CHANGE_REQ 대기 중... (RECIPE_REQ 미감시)");
|
||||||
|
Status.SetWorkingPosition(eWorkingPosition.LOADING_CELL);
|
||||||
|
|
||||||
|
// ── 1) 단일 게이트: PAGE_REQ ON 대기 ──
|
||||||
|
var t0 = DateTime.Now;
|
||||||
|
while (!context.Stopped)
|
||||||
|
{
|
||||||
|
if (_PLC == null) { Log.E("[CheckPageHandshake] _PLC null"); return null; }
|
||||||
|
if (_PLC.ReadBit(eBitInput.PLC_PAGE_CHANGE_REQ).data) {
|
||||||
|
Log.I("---> [CheckPageHandshake] PLC_PAGE_CHANGE_REQ ON");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (DateTime.Now.Subtract(t0).TotalSeconds > PLC_TIMEOUT) { Log.W("[CheckPageHandshake] REQ 대기 TIMEOUT"); t0 = DateTime.Now; }
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
|
if (context.Stopped) return null;
|
||||||
|
|
||||||
|
// ── 2) RecipeNum / PageNum 워드 읽기 (각 3회 검증) ──
|
||||||
|
plcRecipeNo = ReadWordVerified(eWordInput.PLC_RECIPE_NO, 3);
|
||||||
|
plcPageNo = ReadWordVerified(eWordInput.PLC_PAGE_NO, 3);
|
||||||
|
if (context.Stopped) return null;
|
||||||
|
_CurrentPageNo = plcPageNo - 1;
|
||||||
|
|
||||||
|
string cellId = _PLC.CellId(); string lotId = _PLC.LotId();
|
||||||
|
Status.GetProcessingWeldingData().CellId = cellId;
|
||||||
|
Status.GetProcessingWeldingData().LotId = lotId;
|
||||||
|
Log.I($"[CheckPageHandshake] read Recipe={plcRecipeNo}, Page={plcPageNo}, Cell={cellId}, Lot={lotId}");
|
||||||
|
|
||||||
|
// ── 3) 레시피 활성화 (★ 8-3 확인 포인트: 유지 시) ──
|
||||||
|
// 읽은 RecipeNum과 현재 활성 레시피가 다르면 활성화 + 반영 폴링.
|
||||||
|
var recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == plcRecipeNo);
|
||||||
|
if (recipe == null) {
|
||||||
|
Log.E($"[CheckPageHandshake] {plcRecipeNo}번 레시피 없음");
|
||||||
|
MessageUtil.ShowErrorToast($"PLC Recipe No [{plcRecipeNo}] 레시피 없음");
|
||||||
|
Thread.Sleep(3000); return null;
|
||||||
|
}
|
||||||
|
var pre = _RecipeManager.ActivatedRecipe();
|
||||||
|
if (pre == null || pre.Id != recipe.Id) {
|
||||||
|
_RecipeManager.ActivateRecipe(recipe.Id, "PLC Recipe Change");
|
||||||
|
var aS = DateTime.Now;
|
||||||
|
while (!context.Stopped) {
|
||||||
|
var cur = _RecipeManager.ActivatedRecipe();
|
||||||
|
if (cur != null && cur.Id == recipe.Id) break;
|
||||||
|
if (DateTime.Now.Subtract(aS).TotalSeconds > PLC_TIMEOUT) { Log.W("[CheckPageHandshake] 활성 반영 TIMEOUT"); aS = DateTime.Now; }
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
|
if (context.Stopped) return null;
|
||||||
|
Log.I($"[CheckPageHandshake] ActivateRecipe → {recipe.Id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Page 검증 (레시피 활성화 후, 레이어 수 기준) ──
|
||||||
|
int recipePageCount = _ScannerCanvasUserCtrl.Model.Layers.Count();
|
||||||
|
if (plcPageNo < 1 || plcPageNo > recipePageCount) {
|
||||||
|
Log.E($"[CheckPageHandshake] Invalid PageNo {plcPageNo} (활성 레시피 페이지 수 {recipePageCount})");
|
||||||
|
MessageUtil.ShowErrorToast($"Invalid PageNo {plcPageNo}");
|
||||||
|
Thread.Sleep(3000); return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4) 에코: PC→PLC 주소에 읽은 값 그대로 적기 ──
|
||||||
|
_PLC.WriteWord(eWordOutput.PC_RECIPE_NO, plcRecipeNo);
|
||||||
|
_PLC.WriteWord(eWordOutput.PC_PAGE_NO, plcPageNo);
|
||||||
|
Log.I($"---> [CheckPageHandshake] echo PC_RECIPE_NO={plcRecipeNo}, PC_PAGE_NO={plcPageNo}");
|
||||||
|
|
||||||
|
// ── 5) 페이지 COMPLETE 신호만 송신 (레시피 전용 COMP 없음) ──
|
||||||
|
_PLC.WriteBit(eBitOutput.PC_PAGE_CHANGE_COMPLETE, false); // ※ 극성은 8-3 확인 포인트
|
||||||
|
Log.I("[CheckPageHandshake] Set PC_PAGE_CHANGE_COMPLETE → Upload 이동");
|
||||||
|
|
||||||
|
Application.Current.AuroraEnv().EventPublisher.Publish(
|
||||||
|
new EventMessage("", CommonEventIds.EVENT_AUTO_RUN_UPDATE_PAGENUM, (int)_CurrentPageNo));
|
||||||
|
|
||||||
|
Thread.Sleep(5);
|
||||||
|
return SequenceAction.Ok();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 변경 전과의 차이: ① RECIPE_REQ 감시·`PC_RECIPE_CHANGE_COMPLETE` set **제거**, ② 노드 4→1,
|
||||||
|
> ③ 완료 신호는 **PAGE_COMPLETE 하나**. ④ Recipe/Page 워드 에코는 유지.
|
||||||
|
|
||||||
|
### 8-3. ★ 순서 불변식 (현장 확정 — 활성화 단계 필수)
|
||||||
|
|
||||||
|
> **현장 확정(2026-06-20): 레시피 활성화 단계(3)는 반드시 유지한다.**
|
||||||
|
> 읽은 RecipeNum으로 **레시피를 실제 활성화**하고, 그 **활성 레시피의 PageNum 레이어**로 용접리스트를 생성해야 한다.
|
||||||
|
> 즉 노드는 4→1로 줄지만, "바뀐 레시피 + 그 레시피의 해당 페이지"로 용접리스트가 만들어지는 결과는 동일하게 보장돼야 한다.
|
||||||
|
|
||||||
|
핸드셰이크(8-2) → 용접리스트 생성(`SetUploadWeldingList_SeqParams`, 라인 552)으로 이어지는 데이터 흐름:
|
||||||
|
|
||||||
|
```
|
||||||
|
CheckPageHandshake
|
||||||
|
(3) ActivateRecipe(plcRecipeNo) ← 활성 레시피 교체
|
||||||
|
→ _ScannerCanvasUserCtrl.Model.Layers 가 "활성 레시피의 레이어"로 갱신됨
|
||||||
|
(Page 검증) plcPageNo ≤ Layers.Count()
|
||||||
|
→ _CurrentPageNo = plcPageNo - 1
|
||||||
|
│
|
||||||
|
▼ (다음 노드)
|
||||||
|
SetUploadWeldingList_SeqParams (라인 559~566)
|
||||||
|
var layer = _ScannerCanvasUserCtrl.Model.Layers[_CurrentPageNo]; // 활성 레시피 × 해당 페이지
|
||||||
|
_SelectedObjectList = layer.Objects; // 이 페이지의 용접 오브젝트로 리스트 생성
|
||||||
|
```
|
||||||
|
|
||||||
|
**불변식: 반드시 `ActivateRecipe 반영 완료 → Page 검증/`_CurrentPageNo` 확정 → 용접리스트 생성` 순서.**
|
||||||
|
한 노드 안에서 활성화를 먼저 끝내므로(반영 폴링 포함) 이 순서는 자연히 성립한다.
|
||||||
|
|
||||||
|
### 8-3-1. 캔버스 Model.Layers 적재 메커니즘 (코드 추적 결과)
|
||||||
|
|
||||||
|
용접리스트가 읽는 `_ScannerCanvasUserCtrl.Model.Layers`는 **레시피 파일을 Load할 때 재생성**된다.
|
||||||
|
실제 적재 지점은 `MainPage.ReloadRecipe()` 내부의 한 줄:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// WeldingComponents/LASA/GUI/Page/MainPage.cs : ReloadRecipe(fullpath)
|
||||||
|
DB.Dao.WeldingLinesRecipe.Load(fullpath); // ★ 레시피 티칭파일 → DataModel.Layers 재생성
|
||||||
|
DB.Dao.SystemRecipe.WorkRecipeFile = fullpath;
|
||||||
|
UpdateWeldingLinesRecipe(); // → UpdateLayersInfo() (페이지 콤보박스 갱신)
|
||||||
|
LoadFieldScanFromLasaDb(); // (어제 추가한 레시피축 stale 수정: 필드만 로드)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `Layers` 1개 = 페이지 1개, `layer.Objects` = 그 페이지의 용접 오브젝트.
|
||||||
|
- 즉 **"활성 레시피의 레이어"가 되려면 ReloadRecipe(=WeldingLinesRecipe.Load)가 실행돼야** 한다.
|
||||||
|
`_RecipeManager.ActivateRecipe()` **자체는 캔버스 모델을 직접 안 건드린다.**
|
||||||
|
|
||||||
|
### 8-3-2. ★ ActivateRecipe → Layers 재적재 트리거 체인 (2-홉, 비동기)
|
||||||
|
|
||||||
|
```
|
||||||
|
[시퀀스 스레드]
|
||||||
|
_RecipeManager.ActivateRecipe(id, "PLC Recipe Change")
|
||||||
|
│ └ ActivatedRecipe().Id 는 여기서 "동기적으로" 즉시 바뀜 ← 시퀀스 폴링이 보는 값
|
||||||
|
▼ Aurora가 SYS_RECIPE_ACTIVATED 발행
|
||||||
|
[UI Dispatcher 스레드 — 홉①] UCScannerObjectTeachingViewModel.OnActivatedRecipe
|
||||||
|
│ (구독: line 64~66, .ObserveOnDispatcher)
|
||||||
|
├ ActivateModel / SaveRecipe
|
||||||
|
└ EVENT_CHANGE_RECIPE_INFO 발행 (line 159)
|
||||||
|
▼
|
||||||
|
[UI Dispatcher 스레드 — 홉②] RMSHandlerForPTOP.OnUpdateCurrentActivatedRecipe
|
||||||
|
│ (구독: line 56~59, .ObserveOnDispatcher)
|
||||||
|
└ RecipeHelper.ReloadRecipe(info.Name)
|
||||||
|
└ MainPage.ReloadRecipe(fullpath)
|
||||||
|
└ DB.Dao.WeldingLinesRecipe.Load(fullpath) ← 여기서 비로소 Model.Layers 교체 ★
|
||||||
|
```
|
||||||
|
|
||||||
|
> 이 체인은 **RMSHandlerForPTOP가 등록돼 있어야** 동작한다. (EquipmentSetup의 `RchHandler().RegistryDeviceHandler` 경로)
|
||||||
|
|
||||||
|
### 8-3-3. ⚠️ 결론: race 실재 — "활성 id 일치"는 "Layers 교체 완료"를 보장하지 않음
|
||||||
|
|
||||||
|
- 시퀀스의 반영 폴링은 `while (ActivatedRecipe().Id != recipe.Id)` 인데, 이 id는 **ActivateRecipe 호출 즉시(동기)** 바뀐다.
|
||||||
|
→ **폴링은 사실상 바로 통과**한다.
|
||||||
|
- 하지만 `Model.Layers` 교체는 위 **2-홉 UI Dispatcher 이벤트가 실행된 "다음"**이다(시퀀스 스레드와 비동기).
|
||||||
|
- 따라서 폴링 통과 직후 `SetUploadWeldingList_SeqParams`가 `Model.Layers[_CurrentPageNo]`를 읽으면
|
||||||
|
**아직 이전 레시피의 레이어**일 수 있다 → **어제 본 "레시피축 stale"과 동일한 사고**.
|
||||||
|
(관련: `133714_recipe전환시_scout필드_이전레시피_잔존_분석.md`)
|
||||||
|
|
||||||
|
### 8-3-4. 대응안 (택1, 적용 시 결정)
|
||||||
|
|
||||||
|
- **(A) 동기 강제 로드**: 시퀀스에서 활성화 후 `Application.Current.Dispatcher.Invoke(() => RecipeHelper.ReloadRecipe(recipe.Name))`를
|
||||||
|
**직접 호출하여 Layers 교체를 그 자리에서 완료**시킨 뒤 다음 노드로. (가장 확실. 단 자동 체인과 이중 로드 안 되게 정리)
|
||||||
|
- **(B) 로드 완료 신호 폴링**: ReloadRecipe 완료를 알리는 플래그/이벤트를 두고 시퀀스가 그것까지 대기.
|
||||||
|
(Layers 자체엔 레시피 식별자가 없으므로 별도 마커 필요)
|
||||||
|
- **(C) 검증 키 추가**: DataModel/Layer에 레시피 Id를 실어 `Model.RecipeId == plcRecipeNo` 일치까지 폴링.
|
||||||
|
- 어느 안이든 **불변식의 실질 보장은 "ActivatedRecipe().Id 일치"가 아니라 "Layers가 새 레시피로 교체 완료"** 기준이어야 한다.
|
||||||
|
|
||||||
|
### 8-4. 적용 전 확인 포인트
|
||||||
|
|
||||||
|
- [x] **레시피 활성화 단계(3) 유지** — 확정(8-3). 읽은 RecipeNum으로 활성화 + 그 레시피의 PageNum 레이어로 용접리스트 생성.
|
||||||
|
- [x] **활성화 반영 ↔ 캔버스 Layers 갱신 = 비동기 race 확인됨**(8-3-1~3). → 대응안 A/B/C 중 택1 필요(8-3-4).
|
||||||
|
- [ ] **PC_PAGE_CHANGE_COMPLETE 극성**: 현재 코드 관례는 `false` write. PLC 기대값(active-high/low) 재확인.
|
||||||
|
- [ ] **PC_RECIPE_NO 에코만으로 PLC가 만족하는가**(레시피쪽 COMP 비트 정말 불필요한지) 최종 확인.
|
||||||
|
- [ ] 매 사이클 PAGE_REQ를 항상 주는가(단일 블로킹 게이트 전제 충족).
|
||||||
|
|
||||||
|
### 8-5. ★ 매 사이클 동작 vs 조건부 활성화 (현장 확정)
|
||||||
|
|
||||||
|
> 현장 확정(2026-06-20):
|
||||||
|
> - **매 사이클** PAGE_REQ를 받고, **매 사이클** RecipeNum·PageNum을 읽는다.
|
||||||
|
> - **RecipeNum은 더블체크(3회 검증)** 후, **이전 RecipeNum과 다를 때만** 레시피를 활성화한다.
|
||||||
|
> - 레시피마다 **페이지 수가 정해져 있다**(예: 적은 레시피 2개, 많은 레시피 5개). "2~5 사이 임의값"이 아니라 **레시피별 고정값**.
|
||||||
|
> - 단, **페이지 수 상한은 없다 — 사용자가 티칭에서 페이지(레이어)를 얼마든지 추가**할 수 있다(5 초과도 가능).
|
||||||
|
> → 따라서 검증/반복에 **상한 하드코딩 금지**. 오직 `Layers.Count()`(= 그 활성 레시피의 실제 페이지 수)만 신뢰해 가변 처리.
|
||||||
|
|
||||||
|
| 단계 | 빈도 | 비고 |
|
||||||
|
|------|------|------|
|
||||||
|
| PAGE_REQ 대기 / RecipeNum·PageNum 읽기(각 3회) | **매 사이클** | 더블체크 |
|
||||||
|
| 레시피 활성화 + Layers 교체 대기 | **RecipeNum 변경 시에만** | 동일 레시피면 스킵(택트·race 회피) |
|
||||||
|
| Page 검증(`plcPageNo ≤ Layers.Count()`) | **매 사이클** | 활성 레시피의 실제 페이지 수 기준(레시피별 고정값) |
|
||||||
|
| 캔버스 `Layers[_CurrentPageNo]` → 용접리스트 | **매 사이클** | 페이지별 오브젝트 |
|
||||||
|
| 에코(PC_RECIPE_NO/PC_PAGE_NO) + PAGE COMPLETE | **매 사이클** | |
|
||||||
|
|
||||||
|
조건부 활성화 가드(프리뷰 — 8-2의 (3)을 이 형태로):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 이전 사이클 RecipeNum 추적 (필드)
|
||||||
|
private short _prevRecipeNo = -1;
|
||||||
|
|
||||||
|
// ... CheckPageHandshake 내부, RecipeNum 3회 검증으로 plcRecipeNo 확정 후 ...
|
||||||
|
bool recipeChanged = (plcRecipeNo != _prevRecipeNo)
|
||||||
|
|| (_RecipeManager.ActivatedRecipe()?.Id != plcRecipeNo); // 활성상태와도 교차 확인(안전)
|
||||||
|
if (recipeChanged)
|
||||||
|
{
|
||||||
|
var recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == plcRecipeNo);
|
||||||
|
if (recipe == null) { /* 알람 + return null */ }
|
||||||
|
|
||||||
|
_RecipeManager.ActivateRecipe(recipe.Id, "PLC Recipe Change");
|
||||||
|
// ★ Layers 교체 "완료"까지 보장 (8-3-4 대응안 A 권장: Dispatcher.Invoke로 ReloadRecipe 동기화)
|
||||||
|
EnsureLayersReloaded(recipe); // (대응안에 따라 구현)
|
||||||
|
Log.I($"[CheckPageHandshake] RecipeNo {(_prevRecipeNo)}→{plcRecipeNo} 활성화");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.I($"[CheckPageHandshake] RecipeNo {plcRecipeNo} 동일 → 활성화 스킵");
|
||||||
|
}
|
||||||
|
_prevRecipeNo = plcRecipeNo;
|
||||||
|
|
||||||
|
// 이후: Page 검증(plcPageNo ≤ Layers.Count(), 레시피별 고정 페이지 수) → _CurrentPageNo 확정 → 에코 → PAGE COMPLETE
|
||||||
|
```
|
||||||
|
|
||||||
|
> 가드 기준은 **`_prevRecipeNo` 비교(현장 요청)**를 1차로 쓰되, `ActivatedRecipe().Id`와도 교차 확인하면
|
||||||
|
> 외부 경로(수동 전환/RMS)로 활성 레시피가 바뀐 경우까지 안전. 첫 사이클(`_prevRecipeNo == -1`)은 항상 활성화 1회.
|
||||||
@ -140,3 +140,56 @@
|
|||||||
|
|
||||||
## PLC PC I/O Check 완료
|
## PLC PC I/O Check 완료
|
||||||
- PLC ↔ PC I/O Check 완료
|
- PLC ↔ PC I/O Check 완료
|
||||||
|
|
||||||
|
## 15:xx — TeachingRun H알람 인터락 임시 우회 원복 (현장 PLC 연결 완료)
|
||||||
|
- `Machines/ILSOS_PrismaticRD_PTOP/Sequences/Manual/SeqComponentTeachingRun.cs`의 `CheckCondition_Interlock()`에서 현장 PLC 미연결로 임시 주석 처리했던 **High(중대) 알람 인터락 블록(`AlarmManager.HasAlarmLevelHigh()`)을 원복**.
|
||||||
|
- 현장 PLC 연결 완료 + 현장 소스도 원복 완료 → SW도 원복.
|
||||||
|
- 아래 Door/EMO 블록(`#warning to KSJ > 지워도 무방해보임`)은 별개 건이라 그대로 둠.
|
||||||
|
- 커밋 `9330fb2`, `field/ptop-1st` 브랜치 푸시 완료.
|
||||||
|
- 2026_06_19 작업일지 TODO **"알람 인터락 블록 원복 (현장 PLC 정상화 후)"** 항목 해소.
|
||||||
|
|
||||||
|
## 19:xx — SeqAutoRunPTOP Recipe/Page "동시 수신" 핸드셰이크 재구성 분석 (소스 미수정, 확정)
|
||||||
|
|
||||||
|
### 배경 (현장 확인)
|
||||||
|
- PLC/IO 체크 완료.
|
||||||
|
- 현장 PLC가 **Recipe 변경 신호와 Page 변경 신호를 따로 안 주고 동시에** 줌 → 현재 SW의 비대칭 4노드 구조와 안 맞음.
|
||||||
|
- 가접(tack weld)도 곧 추가 예정(어제 분석과 연계).
|
||||||
|
|
||||||
|
### 현재 구조 문제
|
||||||
|
- 현재: `CheckRecipeNo`(논블로킹 peek) → `CheckRecipeNoComp` → `CheckPageNo`(블로킹) → `CheckPageNoComp` **4노드 비대칭**.
|
||||||
|
- Recipe는 가끔/Page는 매 사이클 가정 → 동시 수신 시 완료비트 분리·REQ 드롭 race 위험.
|
||||||
|
|
||||||
|
### 확정 설계 (단일 PAGE 게이트 핸드셰이크, 4노드 → 1노드)
|
||||||
|
- **PAGE_REQ 하나만** 게이트로 사용(RECIPE_REQ 비트 미감시).
|
||||||
|
- 매 사이클: PAGE_REQ 수신 → **RecipeNum·PageNum 워드 각 3회 더블체크** → (변경 시) 레시피 활성화 → Page 검증 → 캔버스 `Layers[_CurrentPageNo].Objects`로 용접리스트 생성 → **PC_RECIPE_NO/PC_PAGE_NO 에코** → **PAGE COMPLETE 하나만** 송신.
|
||||||
|
- **조건부 활성화**: RecipeNum을 더블체크해 **이전 RecipeNum과 다를 때만** `ActivateRecipe`(동일 레시피면 스킵 → 택트·race 회피). `_prevRecipeNo` + `ActivatedRecipe().Id` 교차 확인.
|
||||||
|
- **페이지 수**: 레시피별 고정값이나 **상한 없음(사용자 페이지 추가 가능)** → 상한 하드코딩 금지, `Layers.Count()`만 신뢰(기존 260618 방향과 일치).
|
||||||
|
|
||||||
|
### ★ 코드 추적으로 확인한 핵심 리스크 (race, 실재)
|
||||||
|
- 캔버스 `Model.Layers`는 `MainPage.ReloadRecipe` → `DB.Dao.WeldingLinesRecipe.Load()`에서 재생성됨. `ActivateRecipe` 자체는 모델을 직접 안 건드림.
|
||||||
|
- `ActivateRecipe` → (SYS_RECIPE_ACTIVATED) → `UCScannerObjectTeachingViewModel.OnActivatedRecipe` → (EVENT_CHANGE_RECIPE_INFO) → `RMSHandlerForPTOP.OnUpdateCurrentActivatedRecipe` → `ReloadRecipe` 의 **UI Dispatcher 2-홉 비동기** 체인.
|
||||||
|
- 시퀀스 폴링이 보는 `ActivatedRecipe().Id`는 활성화 호출 즉시(동기) 바뀌지만, **Layers 교체는 2-홉 뒤**라 → 폴링 통과 직후 용접리스트가 **이전 레시피 레이어**를 읽을 수 있음 = 어제 본 "레시피축 stale"과 동일.
|
||||||
|
- 대응안: (A) 시퀀스에서 `Dispatcher.Invoke(ReloadRecipe)` 동기 강제 로드(권장) / (B) 로드 완료 신호 폴링 / (C) DataModel에 레시피 Id 키 추가. **불변식 기준은 "ActivatedRecipe().Id 일치"가 아니라 "Layers 교체 완료".**
|
||||||
|
|
||||||
|
### 가접 연계
|
||||||
|
- 핸드셰이크와 별개 축. Case B(본접 전 선행 패스)면 `ExecuteWelding`에 가접 리스트 선행 발진 추가(어제 132647 문서 참조).
|
||||||
|
|
||||||
|
### 미해결 확인 포인트 (적용 전)
|
||||||
|
- PC_PAGE_CHANGE_COMPLETE 극성(현재 코드 `false` write) PLC 기대값 재확인.
|
||||||
|
- PC_RECIPE_NO 에코만으로 PLC 만족하는지(레시피 COMP 비트 불필요 확정).
|
||||||
|
- Layers race 대응안 A/B/C 택1.
|
||||||
|
|
||||||
|
### 산출물
|
||||||
|
- 분석 문서: `docs/분석/2026/06/20/191351_recipe_page_통합신호_핸드셰이크_재구성_분석.md` (8장 = 현장 확정안)
|
||||||
|
- **소스 미수정** (분석/프리뷰만).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [일일 요약] 06월 20일 동탄 PTOP 작업내용
|
||||||
|
|
||||||
|
1. Menu 영문화 및 미사용 옵션 삭제
|
||||||
|
2. AjinIO isConnected 함수 오류 수정
|
||||||
|
3. recipe 수정 시 Scout field 저장 안되는 현상 수정
|
||||||
|
4. PLC-PC I/O Check 완료
|
||||||
|
|
||||||
|
- **명일 작업**: 실제 Laser 동작 확인 / Recipe & Page Change 소스 수정 및 적용 테스트 / Seq 동작 테스트
|
||||||
|
|||||||
120
docs/작업지시서/2026_06_21_recipe_page_change_구현.md
Normal file
120
docs/작업지시서/2026_06_21_recipe_page_change_구현.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# 작업지시서 — Recipe & Page Change 소스 수정 및 적용 테스트
|
||||||
|
|
||||||
|
- 작성일: 2026-06-20 (명일 2026-06-21 작업용)
|
||||||
|
- 대상: `Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs`
|
||||||
|
- 근거 분석: `docs/분석/2026/06/20/191351_recipe_page_통합신호_핸드셰이크_재구성_분석.md` (8장 = 확정안)
|
||||||
|
- 관련: `132647_uframe_가접_recipe_추가_seqautorun_분석.md`, `133714_recipe전환시_scout필드_이전레시피_잔존_분석.md`
|
||||||
|
- 현재 상태: **소스 미수정** (분석/프리뷰만 존재). 본 지시서대로 구현 착수.
|
||||||
|
|
||||||
|
> 목적: 현장 PLC가 Recipe·Page 신호를 **동시에** 주는 환경에 맞춰, 비대칭 4노드 핸드셰이크를
|
||||||
|
> **단일 PAGE 게이트 1노드**로 통합한다. + 매 사이클 조건부 레시피 활성화 + Layers stale(race) 방지.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 착수 전 확정 사항 (PLC 담당/현장과 먼저 합의)
|
||||||
|
|
||||||
|
> 아래 3개는 **코딩 시작 전에 값이 확정돼야** 한다. 미확정이면 해당 부분 TODO로 두고 진행.
|
||||||
|
|
||||||
|
- [ ] **C-1. PC_PAGE_CHANGE_COMPLETE 극성**: 현재 코드 관례는 `false` write. PLC 기대값(active-high/low) 확인.
|
||||||
|
- [ ] **C-2. 레시피 COMP 비트 불필요 확정**: PC_RECIPE_NO 워드 에코만으로 PLC가 만족하는가(레시피 전용 COMPLETE 비트 폐기 OK?).
|
||||||
|
- [ ] **C-3. Layers race 대응안 택1**: (A) Dispatcher 동기 ReloadRecipe(**권장**) / (B) 로드완료 신호 폴링 / (C) DataModel 레시피Id 키.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 작업 범위 (Scope)
|
||||||
|
|
||||||
|
### 1-1. 변경 대상 파일
|
||||||
|
- `Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs` (메인)
|
||||||
|
- (대응안 A 선택 시) 시퀀스에서 `RecipeHelper.ReloadRecipe` 동기 호출 경로 확인 — `WeldingComponents/.../RecipeHelper.cs`, `MainPage.cs`는 **읽기만**(수정 지양).
|
||||||
|
|
||||||
|
### 1-2. 제거 (통합으로 흡수)
|
||||||
|
- 노드 4개: `CheckRecipeNo`, `CheckRecipeNoComp`, `CheckPageNo`, `CheckPageNoComp`
|
||||||
|
- 상수 4개: `SEQ_CHECK_RECIPENO`, `SEQ_CHECK_RECIPENO_COMPLETE`, `SEQ_CHECK_PAGENO`, `SEQ_CHECK_PAGENO_COMPLETE`
|
||||||
|
- Flow 2줄: `CheckRecipeNo→CheckPageNo`(REQ OFF 우회), 루프백 `CheckCycleStop→CheckRecipeNo`
|
||||||
|
|
||||||
|
### 1-3. 추가
|
||||||
|
- 노드 1개: `CheckPageHandshake` (+ 상수 `SEQ_CHECK_PAGE_HANDSHAKE`)
|
||||||
|
- 필드: `private short _prevRecipeNo = -1;`
|
||||||
|
- 헬퍼: `ReadWordVerified(eWordInput, times)`(3회 검증 공통화), `EnsureLayersReloaded(recipe)`(대응안 A)
|
||||||
|
- Flow: `... > CheckInitCondition > CheckPageHandshake > SetUploadWeldingList...`, 루프백 `CheckCycleStop > CheckPageHandshake`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 구현 절차 (Step)
|
||||||
|
|
||||||
|
### Step 1. 단일 핸드셰이크 노드 골격
|
||||||
|
- `CheckPageHandshake` 추가. 동작 순서:
|
||||||
|
1. `PLC_PAGE_CHANGE_REQ`(0x08) ON 블로킹 대기 (기존 `CheckPageNo` 패턴 재사용, RECIPE_REQ 미감시)
|
||||||
|
2. `PLC_RECIPE_NO`, `PLC_PAGE_NO` 각 3회 더블체크 → `plcRecipeNo`, `plcPageNo`, `_CurrentPageNo = plcPageNo - 1`
|
||||||
|
3. CellId/LotId 읽어 Status 반영 (기존 `CheckPageNoComp` 로직 이식)
|
||||||
|
|
||||||
|
### Step 2. 조건부 레시피 활성화 (가드)
|
||||||
|
- `recipeChanged = (plcRecipeNo != _prevRecipeNo) || (ActivatedRecipe()?.Id != plcRecipeNo)`
|
||||||
|
- `recipeChanged == true` 일 때만:
|
||||||
|
- `recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == plcRecipeNo)` (없으면 알람+`return null`)
|
||||||
|
- `_RecipeManager.ActivateRecipe(recipe.Id, "PLC Recipe Change")`
|
||||||
|
- **`EnsureLayersReloaded(recipe)`** ← Layers 교체 "완료"까지 보장 (대응안 A)
|
||||||
|
- `else` 동일 레시피 → 활성화/재로드 스킵
|
||||||
|
- 끝에 `_prevRecipeNo = plcRecipeNo;`
|
||||||
|
- 첫 사이클(`_prevRecipeNo == -1`)은 항상 1회 활성화.
|
||||||
|
|
||||||
|
### Step 3. ★ Layers race 대응 (대응안 A — EnsureLayersReloaded)
|
||||||
|
- `ActivateRecipe`만으로는 캔버스 `Model.Layers`가 즉시 안 바뀜(UI Dispatcher 2-홉 비동기).
|
||||||
|
- 대응안 A: 시퀀스에서 직접 동기 로드 후 진행.
|
||||||
|
```csharp
|
||||||
|
Application.Current.Dispatcher.Invoke(() => RecipeHelper.ReloadRecipe(recipe.Name));
|
||||||
|
// 이중 로드 방지: 자동 체인(RMSHandlerForPTOP)과 겹치지 않도록 호출 위치/중복 확인
|
||||||
|
```
|
||||||
|
- **검증 기준은 `ActivatedRecipe().Id` 일치가 아니라 `Model.Layers`가 새 레시피로 교체 완료.**
|
||||||
|
|
||||||
|
### Step 4. Page 검증 (레시피 활성화 "후")
|
||||||
|
- `int recipePageCount = _ScannerCanvasUserCtrl.Model.Layers.Count();`
|
||||||
|
- `if (plcPageNo < 1 || plcPageNo > recipePageCount) { 알람/에러 + return null; }`
|
||||||
|
- **상한 하드코딩 금지** — 페이지는 사용자가 얼마든지 추가 가능. `Layers.Count()`만 신뢰.
|
||||||
|
|
||||||
|
### Step 5. 에코 + 완료비트
|
||||||
|
- `WriteWord(PC_RECIPE_NO, plcRecipeNo)`, `WriteWord(PC_PAGE_NO, plcPageNo)`
|
||||||
|
- `WriteBit(PC_PAGE_CHANGE_COMPLETE, <C-1 확정값>)` — **PAGE COMPLETE 하나만** (레시피 COMP 비트 미사용, C-2)
|
||||||
|
- `EVENT_AUTO_RUN_UPDATE_PAGENUM` 퍼블리시(기존 유지)
|
||||||
|
|
||||||
|
### Step 6. Builder/Flow 정리
|
||||||
|
- 1-2 제거, 1-3 추가 반영. `Draw("SeqAutoRunPTOP.pdf")` 갱신 확인.
|
||||||
|
|
||||||
|
### Step 7. (선택, 별도 작업) 가접 패스
|
||||||
|
- 본 지시서 범위 밖. 가접은 Case B면 `ExecuteWelding`에 선행 패스 추가(132647 문서). 별도 지시서로 분리 권장.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 검증 (Test)
|
||||||
|
|
||||||
|
### 3-1. 빌드
|
||||||
|
- [ ] MSBuild Debug|x64 솔루션 빌드 오류 0개.
|
||||||
|
|
||||||
|
### 3-2. 시퀀스 동작 테스트 (Seq 동작 테스트)
|
||||||
|
- [ ] **동일 레시피 + 페이지만 변경** 반복: 레시피 활성화/ReloadRecipe **스킵** 로그 확인, 페이지별 용접리스트 정상.
|
||||||
|
- [ ] **레시피 변경 사이클**: RecipeNum 바뀐 사이클에만 활성화, Layers가 **새 레시피로 교체된 뒤** 용접리스트 생성되는지(=stale 아님) 확인.
|
||||||
|
- [ ] 페이지 수 다른 레시피 전환(예: 2페이지 ↔ 5페이지) 시 `Layers.Count()` 가변 검증 정상.
|
||||||
|
- [ ] PAGE_REQ → 에코(PC_RECIPE_NO/PC_PAGE_NO) → PAGE COMPLETE 핸드셰이크 PLC와 정상 종결.
|
||||||
|
- [ ] 잘못된 PageNo(범위 밖) 수신 시 에러/알람 처리.
|
||||||
|
|
||||||
|
### 3-3. 실제 Laser 동작 확인 (명일 병행)
|
||||||
|
- [ ] 레시피·페이지 정합 상태에서 실제 발진 라인이 해당 페이지 티칭과 일치.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 리스크 / 주의
|
||||||
|
|
||||||
|
- **이중 로드**: 대응안 A의 수동 ReloadRecipe와 자동 체인(RMSHandlerForPTOP)이 겹쳐 2회 로드될 수 있음 → 호출 위치 정리.
|
||||||
|
- **Dispatcher 데드락**: 시퀀스 스레드에서 `Dispatcher.Invoke`(동기) 시 UI 대기 교착 주의 → 필요시 `InvokeAsync`+완료대기 검토.
|
||||||
|
- **COMPLETE 극성 오설정**(C-1) 시 PLC 핸드셰이크 영원히 안 끝남 → 착수 전 확정.
|
||||||
|
- 레시피축 stale은 어제(133714) 필드 저장 경로에서 이미 한 번 겪음 — 같은 비동기 갱신 타이밍 이슈축.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 완료 기준 (Definition of Done)
|
||||||
|
|
||||||
|
- [ ] 4노드 → 1노드 통합, 빌드 0 오류.
|
||||||
|
- [ ] 동일레시피 스킵 / 레시피변경 시 Layers 교체완료 후 용접리스트 생성, 둘 다 런타임 확인.
|
||||||
|
- [ ] PLC 핸드셰이크(에코+PAGE COMPLETE) 정상 종결.
|
||||||
|
- [ ] 실제 Laser 발진이 레시피×페이지 정합.
|
||||||
|
- [ ] 작업일지 기록 + 커밋/푸시.
|
||||||
Loading…
x
Reference in New Issue
Block a user