diff --git a/docs/분석/2026/07/03/185609_레시피별_인터락_점검_분석.md b/docs/분석/2026/07/03/185609_레시피별_인터락_점검_분석.md new file mode 100644 index 0000000..e28eafc --- /dev/null +++ b/docs/분석/2026/07/03/185609_레시피별_인터락_점검_분석.md @@ -0,0 +1,85 @@ +# 레시피별 인터락 점검 분석 (2026-07-03) + +> 소스 미수정. 2개 독립 subagent(교차검증) + 직접 코드 확인(Read/Grep)으로 사실만 정리. +> 범위: `Machines/ILSOS_PrismaticRD_PTOP/Sequences/`, `Alarm/`, `Device/PLC/`, `Recipes/` + +## 0. 결론 요약 (심각도순) + +1. **메인 AutoRun 시퀀스(SeqAutoRunPTOP)의 인터락 체크 진입점이 통째로 죽은 코드(dead code)** — 현재 실제 운용 중인 자동 사이클에서 `CheckCondition_Interlock()` 호출 자체가 발생하지 않음. +2. 다른 시퀀스(ComponentWeld/Prismatic/TestRun/WaitWeldOrChangePage)는 인터락 함수가 **호출은 되지만 항상 `true`를 반환하는 빈 스텁**. +3. 수동 티칭 시퀀스(SeqComponentTeachingRun)는 H알람(Heavy) 체크만 살아있고 **Door/EMO 인터락은 주석 처리**됨. +4. 실질적으로 살아있는 안전 인터락은 **레이저 Head Theta 각도 인터락 1개뿐**(`ExecuteWelding`, fail-closed). +5. **Axis Position Interlock(전축 위치 인터락)은 코드상 전혀 미구현** — 기존 메모 기록과 일치, 여전히 미착수. +6. 가접(Tack) 레시피 번호(20/21/22/23 vs 21/22/23/24)는 **코드에 고정 상수로 존재하지 않음** — 런타임에 PLC가 보내는 번호 그대로 매칭하는 구조라 코드로는 확정 불가. 레시피별로 분기되는 인터락 로직 자체도 없음(인터락이 레시피 번호와 무관하게 전역적으로 스텁). + +--- + +## 1. SeqAutoRunPTOP.cs — 인터락 진입점 자체가 미호출 (Dead Code) + +- `CheckInitCondition`(322-337행)에서 `CheckCondition(context)` 호출이 **통째로 주석 처리**(327-333행). 대신 `Thread.Sleep(5)` 후 바로 `SEQ_CHECK_PAGENO`로 분기(336행). +- `CheckCondition`(1100-1114행)은 `CheckCondition_Interlock()`(1104행) + `InitializeDeviceStatus()`(1106행)를 감싸는 함수지만, 위 주석 처리로 인해 **어디서도 호출되지 않음**. +- `CheckCondition_Interlock()`(1116-1121행) 자체도 `Log.I("...현재 항상 통과")` 후 `return true` 뿐인 스텁(1119-1120행). +- **의미**: 메인 자동운전 사이클 진입 시 인터락 판정뿐 아니라 `InitializeDeviceStatus`(디바이스 초기상태 확인)까지 같이 스킵됨. 스텁이 항상 true를 반환하는 것과, 애초에 호출 자체가 안 되는 것은 결과는 같아도 원인이 다름 — 후자는 "누군가 임시로 주석 처리한 뒤 복구를 잊었을 가능성"을 시사. + +## 2. 다른 시퀀스 — 인터락 함수는 호출되나 항상 true (스텁) + +| 파일 | CheckCondition 호출 | CheckCondition_Interlock 내용 | +|---|---|---| +| `SeqAutoRunComponentWeld.cs` | 251행 호출(주석 없음, 실사용) | 590-595행, 주석만 있고 `return true` | +| `SeqAutoRunPrismatic.cs` | 342행 호출(실사용) | 1158-1163행, `return true` | +| `SeqTestRun.cs` | 489행 호출(실사용) | 507행~, `return true` | +| `SeqWaitWeldOrChangePage.cs` | 397-399행 호출(실사용) | 413행~, `return true` | + +이 4개 시퀀스는 SeqAutoRunPTOP과 달리 함수 호출 자체는 살아있으나, 함수 내부가 애초에 아무 조건도 검사하지 않는 빈 스텁이라 결과적으로 동일하게 "인터락 없음" 상태. + +## 3. SeqComponentTeachingRun.cs — 부분적으로만 살아있음 + +`CheckCondition_Interlock()`(271-296행): +- `AlarmManager.HasAlarmLevelHigh()`(274행) — **살아있음**, High 알람 시 차단. +- `IsPlcDoorOpen()`(282-287행), `IsPlcEmoAlarm()`(289-294행) — **전부 주석 처리**. 281행에 `#warning to KSJ > 지워도 무방해보임` 메모가 남아있어, 의도적으로 비활성화한 것으로 보이나 최종 확정 여부는 불명. +- 최종 `return true`(295행) — Door/EMO 체크가 없으므로 사실상 Door 열림/EMO 눌림 상태에서도 티칭 시퀀스 조건 통과 가능. + +## 4. 실질적으로 동작하는 유일한 인터락 — Head Theta(레이저 각도) + +`SeqAutoRunPTOP.cs` `ExecuteWelding`(744-792행 부근): +- `LaserInterlockSetting.HeadThetaInterlockEnabled`(753행) 체크박스가 켜져 있을 때만 동작. +- PLC 미연결/미활성 시 **fail-closed**(756-760행, `blocked = true`). +- `HEAD_THETA_POSITION`(D70510) 워드를 읽어 수평/수직 기준각 ± 허용오차 범위 밖이면 `ALARM_LASER_HEAD_THETA_INTERLOCK` 발생 + 발진 차단(786-791행). +- 레시피 번호와 무관하게 전역적으로 매 용접마다 적용됨(레시피별 분기 없음). + +## 5. Axis Position Interlock(전축 위치) — 미구현 재확인 + +`AxisPosition`/`전축`/`축위치`/`SectorAngle` 키워드로 `Machines/ILSOS_PrismaticRD_PTOP` 전체 grep → 실 소스 매치 0건(라이브러리 XML 문서 파일만 우연 매치). 기존 메모([[axis-position-interlock-todo]]) 그대로, 판정 로직 여전히 미착수. + +## 6. 가접(Tack) 레시피 번호 & 레시피별 인터락 분기 + +- 코드 내 Tack/U-Frame 전용 레시피 번호 상수 **없음**. `CheckRecipeNoComp`(SeqAutoRunPTOP.cs 346-433행)는 PLC가 보낸 `PLC_RECIPE_NO`를 `_RecipeManager.Recipes()`에서 `.Id` 매칭으로만 처리 — 번호 자체는 런타임/설정 데이터에 있고 코드에 하드코딩되어 있지 않음. +- 과거 기록상 20/21/22/23([[recipe-page-handshake-redesign]])과 21/22/23/24, 20 폐지([[bit15-resume-feature]])가 서로 다르게 남아있는데, **코드로는 어느 쪽도 검증 불가** — 레시피 데이터(엑셀/현장 등록값) 확인 필요. +- 레시피 번호별로 다르게 적용되는 인터락 로직도 코드상 확인되지 않음 — 애초에 인터락 판정 자체가 4장 항목 외엔 전역 스텁이라, "레시피 A는 인터락 있고 B는 없다"는 분기 자체가 존재하지 않음(있으면 안 되는데 지금은 다 스텁이라 무의미). + +## 7. bit15(Weld_01 Ready) Resume — 인터락과의 상호작용 + +- `PC_WELD_READY_ACK`를 false로 되돌리는 곳은 `CheckCycleStop`(872-878행) 한 곳, `ResumeMode`가 true일 때만. +- Resume 트리거·팝업·재개 로직에 레시피 번호(21/22 등) 조건 결합 없음 — 전 레시피 공통 동작. +- Resume 경로가 인터락 체크를 "스킵"하는지 여부는 애초에 1장에서 확인했듯 SeqAutoRunPTOP의 인터락 진입점 자체가 미호출(dead code)이므로, "스킵" 여부를 논하는 것 자체가 무의미 — 인터락이 있었다면 스킵됐을지 확인이 필요하지만 지금은 그 이전 단계에서 이미 빠져있음. + +## 8. PLC 인터락 관련 신호 매핑 (참고, 확실치 않음) + +Agent 조사에 따르면 `PLCController.Address.cs`에 `PLC_DOOR_INTERLOCK`, `PLC_EMO_STATUS`, `PLC_LIGHT_ALARM_STATUS`, `PLC_HEAVY_ALARM_STATUS`, `HEAD_THETA_POSITION` 주소가 매핑되어 있음(직접 재검증은 안 함, 파일:라인 인용은 subagent 보고 기준이라 **확실하지 않음**으로 표시). 즉 PLC 신호 자체는 준비돼 있는데, 이를 실제로 읽어 판정하는 애플리케이션 로직(1~3장)이 대부분 비활성화된 상태로 보임. + +--- + +## 교차검증 메모 + +두 subagent가 독립적으로 조사한 결과가 대부분 일치했으나, SeqAutoRunPTOP의 인터락 상태에 대해 표현이 갈렸음: +- Agent A: "인터락 함수가 항상 true를 반환하는 스텁" +- Agent B: "인터락 함수 자체가 호출되지 않는 dead code" + +직접 `Read`로 322-337행, 1100-1121행을 확인한 결과 **Agent B가 정확**함 — `CheckCondition(context)` 호출이 주석 처리되어 있어 `CheckCondition_Interlock()`은 SeqAutoRunPTOP에서 실행 경로상 도달하지 않음. (다른 4개 시퀀스는 Agent A 설명대로 호출은 되고 내부가 스텁.) + +## 후속 확인 필요 (제안, 미실행) + +- SeqAutoRunPTOP의 327-333행 주석 처리가 의도적 임시 조치인지, 복구를 잊은 것인지 — 커밋 이력/작업지시서 대조 필요. +- SeqComponentTeachingRun의 Door/EMO 인터락 주석 처리(`#warning to KSJ`)가 최종 승인된 사양인지 확인 필요. +- 가접 레시피 실제 번호(20~23 vs 21~24)는 코드가 아닌 현장 레시피 데이터/엑셀 기준으로 재확인 필요. +- 위 모든 항목은 **분석 전용이며 수정 요청 없이는 손대지 않음**(협업 규칙 #2). diff --git a/docs/분석/2026/07/23/Laser_Shot_IO_LaserReady_GuideBeam_분석.md b/docs/분석/2026/07/23/Laser_Shot_IO_LaserReady_GuideBeam_분석.md new file mode 100644 index 0000000..dc41415 --- /dev/null +++ b/docs/분석/2026/07/23/Laser_Shot_IO_LaserReady_GuideBeam_분석.md @@ -0,0 +1,557 @@ +# Recipe Laser Shot — Laser Ready / Guide Beam IO 판정 위치 및 상시 ON 배선 대응 분석 + +**분석 일시:** 2026-07-23 +**대상:** `WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs` +**IO enum:** `WeldingComponents/WeldingCommon/Devices/Laser/LaserIOControl.cs` (`PAIX_PrismaticScanner`) +**상태:** 분석/수정안만 기록. **소스 미수정**(사용자 요청). +**관련:** [[150031_laser_ready_di12_di24_change]] · [[143131_laser_ready_di9_analysis]] · [[124752_guidebeam_trupulse_analysis]] · [[162010_io_interface_map_path_source]] + +--- + +## 0. 요청 (사용자) + +- 현재 프로그램 SOURCE는 수정하지 않음. +- Recipe의 Laser Shot 구간에서 **Laser Ready 버튼 / Guide Beam 버튼**이 물린 IO 배선의 TRUE/FALSE를 읽어 동작 → **IO 몇 번인지** 확인 요청. +- 현장 레이저가 기존과 달라 **배선을 다른 곳에 물렸고, 레이저 컨트롤러를 상시 ON** 상태로 두고 그 값을 읽어 **TRUE로 유지돼야 레시피 출사 시퀀스가 동작**하는 구조로 가야 함. +- 해당 소스 위치 + DIFF(적용 전 검토용) + 작업내용 기록 요청. + +--- + +## 1. 결론 — 현재 사용 중인 IO 번호 + +| 항목 | 방향/번호 | enum 의미 (`PAIX_PrismaticScanner`) | 소스 위치 | +|---|---|---|---| +| **Laser Ready 판정(모니터링)** | **DI 11 && DI 15** | 11 = `CoreLaser_Laser_Output_State`, 15 = `RingLaser_Laser_Output_State` | `UCScannerObjectTeachingControlBoxViewModel.cs:271` | +| **Laser Ready 버튼(토글 출력)** | **DO 9, DO 13** | 9 = `CoreLaser_Control`, 13 = `RingLaser_Control` | 같은 파일 `:552~567` (`PerformCmdLaserReadyCheck`) | +| **Guide Beam 상태 표시** | **DO 17 (읽기)** | 17 = `EX_Red_Control` | 같은 파일 `:270` | +| **Guide Beam 버튼(토글)** | **DO 17 (쓰기)** | 17 = `EX_Red_Control` | 같은 파일 `:534~542` (`PerformCmdLaserGuideBeam`) | +| **출사(TeachingRun) 게이트** | `IsLaserReady == false` → 실행 차단 | — | 같은 파일 `:415~418` (`PerformCmdRunTeachingRun`) | + +> Guide Beam은 **DI가 없어 DO 17 명령 비트를 되읽어 표시**하는 구조(실제 빔 출력 피드백 아님). +> 물리 가이드빔 제어의 정식 경로는 Laser(TruPulse) `ExtPilotOnOff`이며 **현재 미구현** — [[124752_guidebeam_trupulse_analysis]] 참고. + +### 1.1 IO 번호 변천 이력 (Laser Ready 판정) + +``` +DI 9 (CoreLaser_Laser_Ready 단독) — 초기 + → DI 12 && DI 24 (Core/Ring Control_Status) — 260617 결선 기준 + → DI 11 && DI 15 (Core/Ring Laser_Output_State) — 260618 결선 변경, 현재 소스 상태 +``` + +### 1.2 enum 정의 (`LaserIOControl.cs:26~44`) + +```csharp +public enum ePrismaticDIAddress { + CoreLaser_Laser_Ready = 9, + CoreLaser_Error_State, // 10 + CoreLaser_Laser_Output_State, // 11 ← 현재 판정에 사용 + CoreLaser_Control_Status, // 12 + RingLaser_Laser_Ready, // 13 + RingLaser_Error_State, // 14 + RingLaser_Laser_Output_State, // 15 ← 현재 판정에 사용 + RingLaser_Control_Status = 23, // 현장 실제 24 (enum outdated, 미정정) +} +public enum ePrismaticDOAddress { + CoreLaser_Control = 9, + CoreLaser_ErrorClearing, // 10 + RingLaser_Control = 13, + RingLaser_ErrorClearing, // 14 + EX_Red_Control = 17, // Guide Beam 토글에 사용 +} +``` + +> **주의:** DI/DO 인덱스의 실제 의미는 enum이 아니라 **런타임 로드되는 `DI.csv`/`DO.csv` 맵**이 결정한다. +> 맵 경로는 Setting → Device(IODevice) Config의 `IOInterfaceMapDI/DO`. 소스에 하드코딩 없음 → [[162010_io_interface_map_path_source]]. +> 기본 `DI.csv`에서는 12=START SWITCH, 24=FAN 8 ALARM, 25=LASER Ready 로 되어 있어 **enum과 불일치 가능**. 새 배선 번호 확정 시 CSV와 반드시 대조할 것. + +--- + +## 2. 현재 구조가 "상시 ON 레이저"와 안 맞는 이유 + +``` +[Laser Ready 버튼] --WriteDoBit(9,13)--> DO9/DO13 (Core/Ring Control 출력) + | ← 이전 레이저: 이 출력으로 컨트롤러를 Enable +[모니터링 100ms] --ReadDI(11) && ReadDI(15)--> DI11/DI15 (Output State 피드백) +[출사 버튼] --IsLaserReady == false → 차단 +``` + +- 새 레이저는 **컨트롤러가 상시 ON**이고 배선이 다른 DI로 옮겨졌으므로, + - DO9/DO13 토글은 **더 이상 Ready 성립 조건이 아님**(누르지 않아도 Ready여야 함, 반대로 눌러도 무의미). + - 판정은 **새로 물린 DI 비트 하나(또는 AND 조합)를 상시 읽어 TRUE 유지**하는 형태여야 함. +- 즉 코드 변경 포인트는 **271행 판정식**(필수) + **버튼 의미 재정의**(선택) 두 곳뿐이다. + +--- + +## 3. 수정안 (DIFF) — 미적용 + +### 3.1 전제 / 확정 필요 사항 + +- **신규 Ready DI 번호가 아직 미확정**이므로, 아래 DIFF는 번호를 **파일 상단 상수 1곳**으로 뽑아 놓았다. + 현장에서 번호만 확인되면 `DI_LASER_READY_MAIN`(및 필요 시 `DI_LASER_READY_SUB`) 값만 바꾸면 된다. +- Core/Ring 2개를 계속 AND로 볼지, 상시 ON 신호 **1개만** 볼지도 현장 확정 필요. + → DIFF는 `DI_LASER_READY_SUB < 0` 이면 **단일 DI 판정**으로 자동 동작하도록 작성. + +### 3.2 판정부 + 상수 (핵심) + +```diff +--- a/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs ++++ b/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs +@@ (필드 선언부, 예: L78 부근) + private bool _IsLaserReady; + public bool IsLaserReady { get => _IsLaserReady; set => SetProperty(ref _IsLaserReady, value); } ++ ++ // ── Laser Ready 판정 DI (260723: 신규 레이저 = 컨트롤러 상시 ON, 배선 변경) ── ++ // 이력: DI9 → DI12&&DI24(260617) → DI11&&DI15(260618) → 아래 상수(260723~) ++ // * 현장 배선 확정 후 아래 두 상수만 수정할 것. SUB < 0 이면 MAIN 단독 판정. ++ private const int DI_LASER_READY_MAIN = 11; // TODO: 현장 신규 배선 DI 번호로 교체 ++ private const int DI_LASER_READY_SUB = 15; // TODO: 단일 신호면 -1 로 설정 ++ ++ // 상시 ON 배선 미완/점검 시 강제 통과용 (기본 false, 절대 true 커밋 금지) ++ private const bool FORCE_LASER_READY = false; + +@@ OnMonitoring() L262~271 + if (_IODevice == null || !_IODevice.IsConnected()) + { + IsGuidBeam = false; + IsLaserReady = false; + + return; + } + + IsGuidBeam = _IODevice.ReadDO(17); +- IsLaserReady = _IODevice.ReadDI(11) && _IODevice.ReadDI(15); // 변경 후 (260618: IO 결선 변경 12&24 → 11&15) ++ // 260723: 신규 레이저 컨트롤러 상시 ON → Control 출력(DO9/13)과 무관하게 ++ // 상시 입력되는 Ready DI 를 그대로 읽어 판정한다. ++ IsLaserReady = FORCE_LASER_READY ++ || (_IODevice.ReadDI(DI_LASER_READY_MAIN) ++ && (DI_LASER_READY_SUB < 0 || _IODevice.ReadDI(DI_LASER_READY_SUB))); +``` + +### 3.3 Laser Ready 버튼 — 의미 재정의 (선택, 권장) + +상시 ON이면 DO9/13 토글은 불필요하며, 잘못 누르면 오히려 Control 출력을 흔들 수 있다. + +```diff +@@ PerformCmdLaserReadyCheck() L552~567 + private void PerformCmdLaserReadyCheck(object obj) + { +- // 추후 Write bit 파라미터화 해야함 +- if(toggleIO) +- { +- _IODevice._iOUnit.WriteDoBit(9, true); +- _IODevice._iOUnit.WriteDoBit(13, true); +- toggleIO = false; +- } +- else +- { +- _IODevice._iOUnit.WriteDoBit(9, false); +- _IODevice._iOUnit.WriteDoBit(13, false); +- toggleIO = true; +- } ++ // 260723: 신규 레이저는 컨트롤러 상시 ON — Control 출력(DO9/13) 토글은 Ready 성립 조건이 아님. ++ // 오조작으로 Control 출력을 흔들지 않도록 쓰기를 막고 현재 DI 상태만 알린다. ++ // (구 레이저 복귀 시 아래 블록 주석 해제) ++ //if (toggleIO) { ++ // _IODevice._iOUnit.WriteDoBit((int)ePrismaticDOAddress.CoreLaser_Control, true); ++ // _IODevice._iOUnit.WriteDoBit((int)ePrismaticDOAddress.RingLaser_Control, true); ++ // toggleIO = false; ++ //} else { ++ // _IODevice._iOUnit.WriteDoBit((int)ePrismaticDOAddress.CoreLaser_Control, false); ++ // _IODevice._iOUnit.WriteDoBit((int)ePrismaticDOAddress.RingLaser_Control, false); ++ // toggleIO = true; ++ //} ++ MessageUtil.ShowWarningToast( ++ $"Laser 상시 ON 구성: Ready = DI{DI_LASER_READY_MAIN}" ++ + (DI_LASER_READY_SUB < 0 ? "" : $" && DI{DI_LASER_READY_SUB}") ++ + $" → 현재 {(IsLaserReady ? "TRUE" : "FALSE")}"); + } +``` + +### 3.4 출사 게이트 — 변경 없음 + +`PerformCmdRunTeachingRun` L415의 `if (IsLaserReady == false) { ... return; }` 는 그대로 둔다. +3.2에서 `IsLaserReady`가 상시 TRUE로 유지되면 출사 시퀀스는 자동으로 통과한다. + +### 3.5 (별건, 권장) enum 정정 + +```diff +--- a/WeldingComponents/WeldingCommon/Devices/Laser/LaserIOControl.cs ++++ b/WeldingComponents/WeldingCommon/Devices/Laser/LaserIOControl.cs +@@ +- RingLaser_Control_Status = 23, ++ RingLaser_Control_Status = 24, // 현장 실배선 기준(enum 23은 outdated) +``` + +--- + +## 4. 적용 체크리스트 + +- [ ] 현장 신규 배선의 **Ready DI 번호 확정** (단일인지 Core/Ring 2개 AND인지 포함) +- [ ] 로드 중인 `DI.csv`(Setting → IODevice Config `IOInterfaceMapDI`)에서 해당 번호의 라벨 확인 — enum과 불일치 여부 +- [ ] 3.2 판정부 적용 + 상수값 교체 +- [ ] 3.3 버튼 재정의 적용 여부 결정(구 레이저 복귀 가능성 있으면 주석 보존 형태 유지) +- [ ] Recipe 화면에서 컨트롤러 ON 상태만으로 Laser Ready Status = TRUE 표시 확인 +- [ ] TeachingRun(출사) 시퀀스가 버튼 조작 없이 진입되는지 확인 +- [ ] `FORCE_LASER_READY` 는 반드시 false 로 되돌린 뒤 커밋 + +## 5. 잔여 리스크 + +- **Guide Beam(DO 17)** 은 여전히 명령 비트 되읽기 표시 — 신규 레이저에서도 물리 빔이 안 나올 수 있음(정식 경로 `ExtPilotOnOff` 미구현). 신규 레이저 배선에 가이드빔 제어선이 별도로 있으면 DO 번호 재확인 필요. +- DO9/DO13 쓰기를 막으면, 이 출력을 다른 인터락에서 참조 중일 경우 영향 가능 → 현장 배선도 대조 필요(현 소스 내 다른 참조처는 없음). +- `ePrismaticDIAddress` 11/15 는 이름상 `Laser_Output_State`(출사 중 상태)라서, 원래 의미대로면 "출사 중"에만 TRUE. 신규 구성에서는 **Ready 의미의 비트**를 잡는 것이 맞다. + +--- + +# 2차 분석 (동일일자) — 시퀀스 측 레이저(IO) 인터락 + +**요청:** "시퀀스에서도 레이저(IO) 인터락 확인하는 부분이 있을 것이다. Manual 동작은 `SeqComponentTeachingRun` 으로 시킬 것이므로 그쪽도 같이 봐야 한다. `IsLaserReady` 도 수정 대상." +**상태:** 분석만. **소스 미수정.** + +## 6. Manual 출사 경로에서 "레이저 Ready"를 보는 곳 — 3계층 + +| # | 위치 | 판정식 | 실제 효력 | +|---|---|---|---| +| ① 화면 게이트 | `UCScannerObjectTeachingControlBoxViewModel.cs:415` (`IsLaserReady == false` → 차단) | `:271` `ReadDI(11) && ReadDI(15)` | **유일하게 실동작하는 IO 인터락** | +| ② 시퀀스 진입부 | `SeqComponentTeachingRun.cs:473` `RetryUntilSuccess(_Laser.GetLaserReady, 10s, 1)` | `_Laser = context.GetLaser()` → **Aurora Laser(트럼프) 디바이스**, IO 아님 | IO 결선과 무관. 실 레이저 미연결 시 타임아웃/오판 가능 | +| ③ 출사 직전 | `SeqComponentTeachingRun.cs:561` → `LaserReadyResponse()` (`:658~669`) | `return true;` (본문 전부 주석 처리) | **스텁 — 인터락 없음** | + +**결론:** 현재 시퀀스 내부에는 **IO(DI) 기반 Ready 확인이 존재하지 않는다.** +②는 IO가 아닌 레이저 디바이스 호출이고 ③은 항상 true. 실제로 막는 것은 ①(버튼 클릭 시점 1회)뿐이며, +시퀀스 진행 중 Ready 가 떨어져도 검출되지 않는다. +`DoComponentWelding` 에서 실동작하는 인터락은 **Head Theta**(`:572~613`)가 유일하다. + +## 7. "Laser Ready" 정의가 소스에 3벌, 서로 다름 + +| 정의처 | 판정 DI | enum 의미 | +|---|---|---| +| `UCScannerObjectTeachingControlBoxViewModel.cs:271` | DI 11 && DI 15 | `Core/RingLaser_Laser_Output_State` | +| `LaserIOControl.cs:77~85` `GetLaserReady()` | DI 9 && DI 13 | `Core/RingLaser_Laser_Ready` | +| `SeqComponentTeachingRun.cs:473` | (IO 미사용) | Aurora `Laser.GetLaserReady()` | + +상시 ON 레이저로 전환 시 이 셋이 따로 놀아 **화면은 Ready 인데 시퀀스가 막히거나 그 반대** 상황이 생긴다. + +## 8. 부수 발견 + +- **`LaserIOControl` 은 PTOP 에서 dead 코드** + `EquipmentSetup.cs:153` 이 `CommonDeviceIds.IODevice` 를 `PAIX_PTOP` 으로 등록 → + `SeqTestRun.cs:469` 의 `... as LaserIOControl` 는 **항상 null** → `:247 _IOLaser.GetLaserReady()` 는 NRE 경로. + **단, `SeqTestRun` 은 현재 사용하지 않는 파일임(2026-07-23 사용자 확인).** 실동작 리스크 없음 — 정리 대상으로만 기록. +- **Auto 경로에는 레이저 Ready 인터락 자체가 없음** + `SeqAutoRunPTOP.cs:589 LaserReadyResponse()` 는 no-op 이며 **호출처도 없다.** + (`SeqAutoRunPrismatic.cs:363/369` 에만 `_Laser.GetLaserReady` 가 남아 있으나 PTOP 활성 경로 아님) +- **enum 2벌 중복** — `PAIX_PTOP.cs:19~37` 과 `LaserIOControl.cs:26~44` 에 동일 `ePrismaticDIAddress/DOAddress` 존재. + 번호 변경 시 한쪽만 고치면 어긋난다. +- `IsLaserReady` 는 100ms 모니터링 폴링값이라 **버튼 클릭 시점 스냅샷**으로만 쓰인다(구간 감시 아님). + +## 9. 수정 방향 (미적용) — 판정 1곳 + 호출 3곳 + +1. **공용 게이트 신설** — 예: `WeldingComponents/WeldingCommon/Devices/Laser/LaserReadyGate.cs` + - 상수: `DI_LASER_READY_MAIN` / `DI_LASER_READY_SUB`(`< 0` 이면 MAIN 단독) / `FORCE_LASER_READY`(기본 false) + - `public static bool IsReady(IODevice io)` — `io.IsSimul` → true, `io == null`·미연결·예외 → **fail-closed(false)** + - WeldingCommon 에 두면 ViewModel 과 PTOP 시퀀스가 함께 참조 가능 + - ⚠ 구식 csproj → `WeldingCommon.csproj` 에 `` **수동 추가 필요** +2. `UCScannerObjectTeachingControlBoxViewModel.cs:271` → `IsLaserReady = LaserReadyGate.IsReady(_IODevice);` +3. `SeqComponentTeachingRun.cs:473` → `_Laser.GetLaserReady` 대신 `() => LaserReadyGate.IsReady(_IO)` + (기존 줄은 구 레이저 복귀 대비 주석 보존) +4. `SeqComponentTeachingRun.cs:658 LaserReadyResponse()` → `return LaserReadyGate.IsReady(_IO);` + → 출사 직전 재확인 부활 +5. `LaserIOControl.GetLaserReady()` 도 동일 게이트로 위임 → 정의 통일 (미사용이지만 정의 분기 제거 목적) +6. `PerformCmdLaserReadyCheck`(DO 9/13 토글)은 상시 ON 에서 무의미 → 쓰기 차단 + 현재 DI 상태 토스트(§3.3 안) + +### 9.1 적용 순서 / 리스크 + +- **4번은 마지막에.** 지금 "항상 통과"를 "DI 미충족 시 타임아웃 차단"으로 바꾸는 변경이라 + **Ready DI 번호 확정 전에 넣으면 현장 출사가 막힌다.** +- 권장 순서: `DI.csv` 라벨 대조 → 번호 확정 → 상수 교체 → **1·2·3 적용** → 화면 Ready TRUE 유지 확인 → **4 적용** → 5·6. +- **Auto 경로(SeqAutoRunPTOP)에 게이트 추가는 별도 판단.** 현재 인터락이 없으므로 넣는 순간 + 양산 정지 리스크가 새로 생긴다(현장 합의 후 결정). + +--- + +# 3차 — 아키텍처 검토 (학습용 정리) + +**주제:** "IO 보드를 직접 읽어 레이저를 제어" vs "Aurora `Laser` 추상화 계층을 통해 제어" +**상태:** **소스 수정 없음.** 개념 정리 + 다음 프로젝트 방향성 기록. +**결론 한 줄:** 기능적으로는 지금 방식도 문제없다. 차이는 **유지보수성·추적성**이며, "매번 소스 수정" 문제는 계층과 **별개**로 핀 번호 외부화로 풀어야 한다. + +## 10. 논의 배경 (2026-07-23) + +- **차장님 의견:** 추상화 레벨이 `Laser` 클래스이므로, **디바이스 등록 시점에 IO 보드를 Laser에 함께 물리고(주입) IO 주소/비트 맵도 같이 등록해서 한 곳에서 관리**하는 게 맞았다. +- **현재 PTOP 실상:** IO 보드는 `IODevice` 로 따로 등록하고, 화면·시퀀스가 그 **address 값을 직접 읽어 개별 제어**한다. + (PTOP 는 처음부터 "IO 보드로 레이저 출사 제어" 구조로 출발 → DI 핀 확인해서 그때그때 소스 수정해 옴) +- **받은 답변(정리):** "IO Device 는 IO Device 대로 쓰고, Laser Device 가 IO Device 에 **의존적으로** 동작하는 구조가 정석이다. + 다만 **이력을 모르면 따라가기 어렵다는 점**이 문제일 뿐, **사용하는 데는 지장 없다.**" +- **방향성:** 다음부터는 등록 시점에 IO 를 Laser 에 주입하는 형태로 작업. **이번 건은 소스 수정하지 않고 나중에 집중해서 진행.** + +## 11. Aurora 가 이미 제공하는 구조 + +### 11.1 계층 + +``` +Aurora.Api.Device + └─ AuroraLaser.Laser ← 장비 코드가 쓰는 유일한 창구(추상화 레벨) + └─ ILaser (인터페이스) ← Laser.cs:555 + ├─ LaserTruFiber (OPC UA, 통신형) + ├─ TruPulse (통신형) + ├─ LaserIPG_IO (IO 보드형) ← IODevice 주입 + ├─ LaserArm_IO (IO 보드형) ← IODevice 주입 + └─ LaserTrumpf_IO (IO 보드형) ← IODevice 주입 +``` + +### 11.2 IO 주입 지점 — `AuroraDevices/AuroraLaser/Laser.cs:60~103` + +```csharp +switch (Config.Type) { + ... + case eLaserType.IPG_YLS: // :90~95 + var obj = ...DeviceManager["IO_Board"] as IODevice; + _Laser = new LaserIPG_IO(DeviceId, IsSimul, obj); + break; + case eLaserType.Coherent_Arm_IO: // :96~99 + obj = ...DeviceManager["IO_Board"] as IODevice; + _Laser = new LaserArm_IO(DeviceId, IsSimul, obj); + break; + case eLaserType.Trumpf_IO: // :100~103 + obj = ...DeviceManager["IO_Board"] as IODevice; + _Laser = new LaserTrumpf_IO(DeviceId, IsSimul, obj); // ★ IO 보드를 Laser 에 주입 + break; +} +``` + +즉 **"IO 를 Laser 에 물려서 하나로 가져간다"는 패턴은 이미 프레임워크에 구현되어 있다.** +`Config.Type`(= `LaserConfig` 세팅값) 하나로 통신형/IO형이 갈린다. + +### 11.3 IO형 구현체가 하는 일 — `Trumpf/LaserTrumpf_IO.cs` + +`ILaser` 의 모든 동작을 **DI/DO 읽기·쓰기로** 구현한다. + +| ILaser 메서드 | 내부 동작 | 위치 | +|---|---|---| +| `LaserOn()` | `LASER_READY_ENABLE` DO ON → `LASER_READY` DI 대기 → `PSTART_STATICAL` DO ON → `PROG_ACTIVE` DI 대기 | `:77~135` | +| `LaserOff()` | `PSTART_STATICAL` OFF → Ready/Complete DI 확인 → `LASER_READY_ENABLE` OFF | `:137~158` | +| `GetLaserReady()` | `PROG_ACTIVE` DI 읽기 | `:271~285` | +| `PilotLaserOn/Off()` | `PILOT_LASER_ON` DO 토글 (Ready/Active 중이면 거부) | `:193~229` | +| 상태 폴링 | 300ms 스레드 `UpdateLaserStatus()` → Ready 면 `LaserOnLampOn` DO 자동 갱신 | `:248~255`, `:416~434` | + +→ 지금 ViewModel 이 손으로 하고 있는 일(DI 폴링, DO 토글, 램프 표시)이 **이미 여기 다 들어있다.** + +## 12. 현재 PTOP 구조 vs 정석 구조 + +### 12.1 현재 (PTOP) + +``` +EquipmentSetup.RegisterDevices() + registry.Register(IODevice, new PAIX_PTOP(...)) :153 ← IO 따로 + registry.Register(Laser, new Laser(...)) :155 ← Laser 따로 (Config.Type 이 IO형 아님) + +[화면] UCScannerObjectTeachingControlBoxViewModel + └ _IODevice.ReadDI(11) && ReadDI(15) :271 ← 직접 읽기 + └ _IODevice._iOUnit.WriteDoBit(9/13/17, ...) :541,557 ← 직접 쓰기 +[시퀀스] SeqComponentTeachingRun + └ _Laser.GetLaserReady() :473 ← IO 와 무관한 통신형 경로 + └ LaserReadyResponse() → return true :658 ← 스텁 +[미사용] LaserIOControl.GetLaserReady() = DI9 && DI13 ← 또 다른 정의 +``` + +**증상:** Ready 정의 3벌, 핀 번호 산재, 이력(DI9 → DI12&24 → DI11&15)이 코드 주석에만 남음. + +### 12.2 정석 (Aurora 패턴) + +``` +registry.Register(IODevice, new PAIX_PTOP(...)) ← IO 는 IO 대로 등록(유지) +registry.Register(Laser, new Laser(...), LaserConfig{ + Type = eLaserType.XXX_IO, ← IO형 지정 + (핀 매핑도 Config 로) ← 등록 시점에 함께 관리 +}) + └ Laser.DoActivate() 가 IODevice 를 찾아 ILaser 구현체에 주입 + +[화면/시퀀스] 모두 _Laser.GetLaserReady() / LaserOn() / PilotLaserOn() 만 호출 +``` + +**핵심:** IO Device 를 없애는 게 아니라, **Laser Device 가 IO Device 에 의존(주입)** 하게 만들고 +**바깥에서는 Laser 만 보이게** 하는 것. (= 받은 답변의 요지) + +### 12.3 두 방식 비교 + +| 항목 | 현재(IO 직접 제어) | 정석(Laser 추상화) | +|---|---|---| +| 동작 결과 | **동일** — 실제로 잘 돌아감 | 동일 | +| 코드량 | 당장은 적음(바로 ReadDI) | 초기 셋업 필요 | +| Ready 정의 위치 | **3곳에 분산** | 1곳(`ILaser` 구현체) | +| 핀 변경 영향 범위 | 화면·시퀀스 여러 곳 | 구현체/Config 1곳 | +| 레이저 교체 시 | 코드 수정 | `Config.Type` 교체 | +| 상태 폴링/램프 | 화면에서 수동 구현 | 구현체 스레드가 담당 | +| **이력 추적** | **어려움**(주석 의존) | 구현체 1곳에 응집 | +| 시뮬레이션 | 개별 가드 필요 | `IsSimul` 일괄 처리 | + +> **"사용하는 덴 아무 상관없다"의 정확한 의미:** 기능적으로는 두 방식이 동치다. +> 차이가 나는 건 **6개월 뒤·다른 사람이 볼 때**다. 지금처럼 배선이 3번 바뀐 이력이 주석에만 남으면 +> "왜 DI 11 인가"를 코드에서 재구성할 수 없다. 그게 이번 건에서 실제로 겪은 비용. + +## 13. 주의 — 계층을 옮겨도 "매번 소스 수정"은 안 없어진다 + +`LaserTrumpf_IO` **역시 핀 번호가 enum 하드코딩**이다(`ePTDIAddress` / `ePTDOAddress`, `:101 :126 :279` 등). +Trumpf_IO 로 갈아타도 **배선이 바뀌면 똑같이 소스 수정 → 빌드 → 배포**가 필요하다. + +정리하면 **문제는 2개, 해법도 2개**다. + +| 문제 | 증상 | 해법 | +|---|---|---| +| **(A) 제어 경로 분산** | Ready 정의 3벌, 추적 불가 | `Laser`(ILaser) 로 일원화 — §12.2 | +| **(B) 핀 번호가 코드에 박힘** | 배선 변경 시 재빌드/재배포 | **핀 번호 외부화** — §14 | + +**(A) 만 해결하면 구조는 깨끗해지지만 여전히 소스를 고쳐야 하고, +(B) 만 해결하면 소스는 안 고쳐도 되지만 정의는 계속 흩어져 있다. 둘은 독립이다.** + +## 14. (B) 핀 번호 외부화 — 이미 쓸 수 있는 수단 + +### 옵션 1 (추천, 재빌드 0) — `DI.csv` 라벨 조회 + +`AuroraDevices/AuroraIO/IOUnit.cs:367` 에 **이미 존재**: + +```csharp +public int GetDiIndex(string id) // ListDI 에서 ID 로 인덱스 검색, 없으면 -1 +public int GetDoIndex(string id) // :380 +``` + +`ListDI/ListDO` 는 런타임에 `IOUnitConfig.IOInterfaceMapDI/DO` (CSV)를 읽어 채워진다(`IODevice.cs:252`). +→ 코드에는 번호 대신 **문자열 키**("LASER Ready" 등)만 두고, 배선 변경 시 **CSV 한 줄만 수정**. +**소스 수정·빌드·배포 전부 불필요.** + +- 전제: 현장 `DI.csv` 에 해당 신호가 이름으로 등재돼 있어야 한다. +- 현 상태 주의: 기본 CSV 는 `12=START SWITCH / 24=FAN 8 ALARM / 25=LASER Ready` 로 **enum 과 이미 어긋나 있다**(§1 주의 참고). +- 반환 `-1` 처리(미등재) 시 fail-closed 필요. + +### 옵션 2 (현장 접근성 최고) — 세팅 화면 노출 + +PTOP 에 이미 `Settings/LaserInterlockSetting.cs` 가 있고 **PropertyGrid 로 세팅 메뉴에 자동 노출**된다 +(Head Theta 인터락이 이 방식). 여기에 `Laser Ready DI (Main/Sub)` 필드를 추가하면 +**현장에서 숫자만 바꿔 저장 → 재빌드 없음.** CSV 편집보다 접근이 쉽다. + +### 옵션 3 (정공법, 중장기) — `LaserConfig` 확장 + IO형 일원화 + +`LaserConfig` 에 핀 매핑을 넣고 `eLaserType` 에 PTOP 용 IO 타입 추가 → (A)+(B) 동시 해결. +**리스크:** `AuroraLaser` 는 **공용 프로젝트**라 타 장비 회귀 영향. 충분한 시간 확보 후 진행. + +### 권고 순서 + +``` +[즉효] 옵션 1 or 2 로 핀 외부화 + 판정을 게이트 함수 1곳으로 수렴 + ↓ (게이트를 두면 호출부는 그대로 두고 내부만 교체 가능) +[중장기] 옵션 3 — 게이트 내부를 _Laser.GetLaserReady() 위임으로 바꿔 Laser 계층에 흡수 +``` + +## 15. 다음 프로젝트 적용 가이드 (체크리스트) + +- [ ] IO 보드로 제어하는 레이저면 **처음부터 `eLaserType.*_IO` 계열로 등록**하고 `Laser` 만 호출한다. +- [ ] 등록 시점에 **IO 디바이스 + 핀/비트 맵을 함께 지정**한다(Config 또는 CSV 키). +- [ ] 화면·시퀀스에서 `IODevice.ReadDI/WriteDoBit` 직접 호출 **금지**(레이저 관련 한정). +- [ ] 핀 번호는 코드 상수로 두지 않는다 → CSV ID 또는 Setting. +- [ ] 배선 변경 이력은 주석이 아니라 **Config/CSV 이력**으로 남긴다. +- [ ] 시뮬/미연결 정책(fail-closed)은 구현체 한 곳에서 정한다. + +## 16. 확인 필요 (숙제) + +- [ ] `CommonDeviceIds.IODevice` 의 **실제 문자열 값**이 `"IO_Board"` 인지. + `Laser.cs:93/97/101` 이 `DeviceManager["IO_Board"]` 로 **하드코딩 조회**하므로, 값이 다르면 IO형 전환 시 `obj == null` → NRE. + (Aurora.dll 내부 상수라 소스로 확인 불가 → 런타임 로그/디컴파일 필요) +- [ ] PTOP 의 현재 `LaserConfig.Type` 실제 설정값(세팅 파일) +- [ ] `Trumpf_IO` 의 `ePTDIAddress/ePTDOAddress` 핀 배치가 PTOP 실배선과 맞는지 (안 맞으면 신규 IO형 구현체 필요) +- [ ] 현장 `DI.csv` 에 신규 Ready 신호가 **이름으로 등재**돼 있는지 → 옵션 1 가능 여부 판단 +- [ ] `LaserTrumpf_IO.GetLaserReady()` 는 `IsSimul` 일 때 **false** 를 반환(`:273~274`) — 시뮬 개발 PC 에서 출사 막힘. 전환 시 정책 확인. + +## 17. 한 줄 요약 + +> **IO Device 는 IO Device 대로 두고, Laser Device 가 IO Device 에 의존(주입)하게 만들어 +> 바깥에서는 `Laser` 만 호출하는 것이 Aurora 의 정석. +> 지금 PTOP 방식도 동작에는 문제없지만, 이력이 코드에 남지 않아 추적이 어렵다. +> 그리고 "매번 소스 수정" 은 계층이 아니라 **핀 번호 외부화**로 풀어야 한다.** + +--- + +# 4차 — 이설·다현장 대응 관점 (§10~§17 의 짝) + +**질문(사용자):** "A공장에 맞춰 동작시켜도 장비를 B공장으로 옮겨 그 시설에 맞추면 바뀌는 부분이 분명 있을 텐데, 이렇게(소스 수정으로) 대응하는 게 맞나?" +**답:** **소스 수정으로 대응하는 것은 맞지 않다.** 다만 "전부 설정화" 도 정답이 아니므로 **분류 기준**이 먼저다. +§10~§17 이 *"왜 추상화인가"* 라면, §18 은 *"왜 설정화인가"* 다. + +## 18. 값의 종속성에 따른 3계층 분리 + +| 계층 | 무엇이 바뀌나 | 있어야 할 곳 | 이설(A→B) 시 | +|---|---|---|---| +| **① 설비 고유·불변** | 용접 시퀀스 흐름, 안전 판정 **알고리즘**, PLC 핸드셰이크 규약 | **코드** | 안 바뀜 | +| **② 호기·현장 가변** | IO 핀 매핑, PLC IP/국번, 축 원점·오프셋, 스케일, 파일 경로, 타임아웃 | **설정**(CSV/json/DeviceConfig) | **여기만 교체** | +| **③ 생산 가변** | 파워·속도·좌표·페이지 | **레시피/모델** | 품종 따라 | + +**현 문제:** ②에 속하는 값(Laser Ready DI 11/15)이 ①(코드)에 들어가 있다. +그 결과 같은 공장에서 **레이저만 교체해도 재빌드**가 필요해졌고, 실제로 이번 한 건에서만 +`DI9 → DI12&&DI24 → DI11&&DI15` 로 **소스를 세 번 고쳤다.** + +## 19. Aurora 는 이미 ②의 자리를 마련해 두었다 + +| 프레임워크 자산 | 위치 | 용도 | +|---|---|---| +| `IOUnitConfig.IOInterfaceMapDI/DO` | `AuroraIO/IOUnitConfig.cs:9` | DI/DO CSV 경로 지정 | +| CSV 런타임 로드 | `AuroraIO/IODevice.cs:252` | 핀 라벨↔인덱스 테이블 구성 | +| `IOUnit.GetDiIndex(string)` / `GetDoIndex(string)` | `AuroraIO/IOUnit.cs:367 / :380` | **이름으로 인덱스 조회**(미등재 시 `-1`) | +| `DeviceManager.GetDeviceConfig()` / `SettingEntry` | Aurora | 장비별 설정 저장·로드, PropertyGrid 자동 노출 | + +> **방증:** `DI.csv` / `DO.csv` 는 **이 저장소에 존재하지 않는다.** +> 원래부터 소스가 아니라 **현장 데이터**로 관리되도록 설계된 파일이다. +> 프레임워크는 "핀 번호는 데이터" 라고 이미 선언해 뒀는데, PTOP 가 그 자리를 안 쓰고 코드에 박은 상태. + +## 20. 이설 시 실제로 바뀌는 항목 (대부분 ②) + +- PLC IP/국번, 통신 포트, MES/호스트 주소 +- 전장 재배선에 따른 **DI/DO 번호**(도어, EMO, 레이저 Ready 등) +- 축 원점·좌표 오프셋, 스캐너 보정 파일 +- 로그/레시피 저장 경로 +- 타임아웃(설비 속도·유틸리티 차이) + +이것들이 코드에 있으면 **이설 = 재빌드 + 재배포 + 재검증** 이 되고, +현장에서 급히 고친 값이 소스에 남아 **다음 호기로 전파**된다. +(`FORCE_LASER_READY` 같은 점검용 플래그가 위험한 이유도 동일 — §3.2/§4 체크리스트 참고) + +## 21. 권장 형태 — "현장 프로파일" 분리 + +``` +소스(불변) ─┐ + ├─ 실행 +현장 프로파일 ─┘ ← 이설 시 이 폴더만 교체 + ├ DI.csv / DO.csv (핀 매핑) + ├ DeviceConfig (PLC IP, 포트, 시뮬 여부) + ├ *Setting.json (인터락 각도·허용오차·Ready DI 번호) + └ 보정/오프셋 파일 +``` + +**반드시 함께 넣어야 하는 2가지** (없으면 설정화가 오히려 위험해진다) + +1. **기동 시 매핑 검증 + 로그** + 부팅 시 `"Laser Ready = DI11 && DI15 (from DI.csv)"` 를 **로그·화면에 표시**. + `GetDiIndex()` 가 `-1`(미등재) 이면 **fail-closed + 알람**. + → 설정화의 최대 위험은 "값이 조용히 틀린 채 도는 것" 이며, 이 검증이 그것을 막는다. +2. **프로파일 형상관리** + 현장 프로파일을 공장/호기별로 Git 또는 백업에 남긴다. + 현재는 CSV 이력이 어디에도 없어 **"A공장은 왜 11이었나"를 재구성할 수 없다.** + +## 22. 주의 — 무조건 설정화는 답이 아니다 + +- **안전 인터락의 판정 로직 자체**(무엇을 AND 할지, fail-closed 정책)는 **코드에 둔다.** + 현장에서 바꿀 수 있게 하면 사고 시 책임 소재가 사라진다. + 현장이 바꿀 수 있는 범위는 **"어느 핀이 그 신호인가"까지**. +- 설정 항목이 늘수록 **조합 검증 난이도**가 올라간다. ② 로 뺄 대상은 *현장마다 실제로 달라지는 것*만. +- 설정 변경은 **엔지니어 권한**으로 제한하고 **변경 이력을 로그**로 남긴다. + +## 23. 이번 건 결론 — 진행 순서 + +Ready DI 번호가 **아직 미확정**이므로, 지금 소스에 박으면 확정 후 **또 고치게 된다.** + +``` +1) 판정을 게이트 함수 1곳으로 수렴 ← 이후 호출부는 건드리지 않아도 됨 +2) 게이트 내부 = DI.csv 라벨 조회(옵션 1) ← 현장은 CSV 만 수정, 재빌드 0 +3) CSV 등재가 어려우면 Setting 노출(옵션 2)로 대체 +4) 기동 시 매핑 검증·로그 추가 ← §21-1 +5) 안정화 후 게이트 내부를 Laser 계층 위임으로 교체 ← §14 옵션 3 +``` + +**소스 수정은 하지 않음.** 위 순서는 DI 번호 확정 이후 집중 작업으로 진행. diff --git a/docs/분석/2026/07/23/레이저제어_구조_학습노트.md b/docs/분석/2026/07/23/레이저제어_구조_학습노트.md new file mode 100644 index 0000000..959824d --- /dev/null +++ b/docs/분석/2026/07/23/레이저제어_구조_학습노트.md @@ -0,0 +1,276 @@ +# 학습노트 — 레이저 제어 구조 (IO 직접 제어 vs Laser 추상화) + +**작성:** 2026-07-23 +**용도:** 혼자 읽으며 공부하기 위한 정리본. 소스를 안 열어도 흐름이 잡히도록 **핵심 코드를 여기 옮겨 놓았다.** +**원본 분석서:** `Laser_Shot_IO_LaserReady_GuideBeam_분석.md` (§1~§23) — 근거·표·체크리스트는 그쪽에 있음 + +--- + +## 0. 30초 요약 + +1. Aurora 에는 **`Laser` 라는 추상화 계층**이 있고, 그 아래 `ILaser` 구현체가 통신형/IO형으로 갈린다. +2. **IO 보드로 레이저를 제어하는 구현체도 이미 있다**(`LaserTrumpf_IO` 등). IO 보드를 Laser 에 **주입**하는 구조. +3. PTOP 는 그 계층을 안 쓰고, **화면·시퀀스가 IO 보드를 직접 읽어** 레이저를 제어한다. +4. 동작에는 문제없다. 문제는 **Ready 정의가 3벌로 흩어지고, 배선 변경 이력이 코드에 안 남는 것**. +5. 그리고 **"배선 바뀔 때마다 소스 수정" 은 계층 문제가 아니라 핀 번호가 코드에 박혀 있어서**다. 둘은 별개 문제. + +--- + +## 1. 먼저 알아야 할 그림 — Aurora 의 레이저 계층 + +``` +Aurora.Api.Device + └─ AuroraLaser.Laser ← 장비 코드가 쓰는 유일한 창구 + └─ ILaser (인터페이스) + ├─ LaserTruFiber 통신형 (OPC UA) + ├─ TruPulse 통신형 + ├─ LaserIPG_IO IO 보드형 ← IODevice 주입 + ├─ LaserArm_IO IO 보드형 ← IODevice 주입 + └─ LaserTrumpf_IO IO 보드형 ← IODevice 주입 +``` + +**포인트:** 통신으로 제어하든 IO 릴레이로 제어하든, **바깥에서 보이는 얼굴은 `Laser` 하나**다. +`LaserOn()`, `GetLaserReady()` 를 부르면 되고, 그게 시리얼인지 DO 비트인지는 구현체가 알아서 한다. + +--- + +## 2. 코드 읽는 순서 (이 순서대로 보면 이해가 빠름) + +| 순서 | 파일 | 볼 것 | +|---|---|---| +| ① | `AuroraDevices/AuroraLaser/Trumpf/LaserTrumpf_IO.cs` | IO형 구현체가 **DI/DO로 뭘 하는지** (§3) | +| ② | `AuroraDevices/AuroraLaser/Laser.cs:60~103` | **타입 하나로 구현체가 갈리는 지점**, IO 주입 (§4) | +| ③ | `Machines/.../EquipmentSetup.cs:153~156` | PTOP 가 **어떻게 등록했는지** (§5) | +| ④ | `WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs:262~281` | 화면이 **직접 DI 읽는 코드** (§5) | +| ⑤ | `Machines/.../Sequences/Manual/SeqComponentTeachingRun.cs:473, 658` | 시퀀스의 Ready 확인부 (§6) | +| ⑥ | `AuroraDevices/AuroraIO/IOUnit.cs:367` | **이름으로 핀 찾기** — 설정화의 열쇠 (§8) | + +①을 먼저 보는 이유: "지금 화면에서 손으로 하는 일이 저기 이미 다 구현돼 있네" 를 먼저 느끼면 나머지가 쉽다. + +--- + +## 3. ① IO형 구현체는 무슨 일을 하나 — `LaserTrumpf_IO.cs` + +`ILaser` 의 모든 동작을 **DI 읽기 / DO 쓰기**로 구현해 놓았다. + +```csharp +public int LaserOn() // :77 +{ + SetLaserDOCommand(ePTDOAddress.LASER_READY_ENABLE, true); // DO ON + Thread.Sleep(500); + SetProgBit(""); // 프로그램 번호를 3비트로 출력 + while (!bResult) { // 타임아웃 10s + GetLaserDIStatus(ePTDIAddress.LASER_READY, out bResult); // DI 로 Ready 대기 + } + SetLaserDOCommand(ePTDOAddress.PSTART_STATICAL, true); // 출사 시작 DO + while (!bResult) { + GetLaserDIStatus(ePTDIAddress.PROG_ACTIVE, out bResult); // DI 로 Active 확인 + } +} + +public bool GetLaserReady() // :271 +{ + if (_IsSimul) return false; // ★ 시뮬이면 false (주의, §9) + GetLaserDIStatus(ePTDIAddress.PROG_ACTIVE, out bActiveResult); + return bActiveResult; +} + +private void UpdateLaserStatus() // :248 +{ + while (GetConnected()) { + Thread.Sleep(300); // 300ms 폴링 스레드 + GetLaserAllStatus(); // Ready 면 LaserOnLampOn DO 자동 갱신 + } +} +``` + +**여기서 얻을 것:** +- 지금 ViewModel 이 `OnMonitoring` 에서 손으로 하는 **DI 폴링 + 램프 표시**가 이미 여기 있다. +- Ready 대기·타임아웃·시뮬 처리 같은 **정형 패턴이 한 곳에 응집**돼 있다. +- 다만 핀 번호는 `ePTDIAddress` **enum 하드코딩** — 이게 §7 의 함정. + +--- + +## 4. ② 구현체는 어디서 갈리나 — `Laser.cs:60~103` + +```csharp +protected override int DoActivate(IDeviceConfig config) +{ + Config = config as LaserConfig; + switch (Config.Type) { // ← 세팅값 하나로 결정된다 + case eLaserType.TruFiber: + _Laser = new LaserTruFiber(DeviceId, IsSimul, Config.Opcua_Slot_No, SocketProtocol.TCP); + break; + case eLaserType.Trumpf_IO: // :100 + obj = Application.Current.AuroraEnv().DeviceManager["IO_Board"] as IODevice; + _Laser = new LaserTrumpf_IO(DeviceId, IsSimul, obj); // ★ IO 보드 주입 + break; + } + return _Laser.Open(Config); +} +``` + +**여기서 얻을 것:** +- "IO 를 Laser 에 물려서 하나로 관리" = **이미 프레임워크에 있는 패턴**. 새로 만들 게 아니다. +- 레이저를 교체하면 이상적으로는 **`Config.Type` 만 바꾸면 된다.** +- ⚠ `DeviceManager["IO_Board"]` — **디바이스 ID가 문자열로 하드코딩.** PTOP 등록 ID 와 다르면 `obj == null` → NRE. + (`CommonDeviceIds.IODevice` 실제 값 확인이 숙제) + +--- + +## 5. ③④ PTOP 는 실제로 어떻게 하고 있나 + +**등록** — `EquipmentSetup.cs` +```csharp +registry.Register(CommonDeviceIds.IODevice, () => new PAIX_PTOP(...), new IOUnitConfig()); // :153 IO 따로 +registry.Register(CommonDeviceIds.Laser, () => new Laser(...), new LaserConfig(), ...); // :155 Laser 따로 +``` +→ **둘이 서로 모른다.** `Config.Type` 이 IO형이 아니라서 `_Laser` 는 통신형 구현체가 붙는다. + +**화면** — `UCScannerObjectTeachingControlBoxViewModel.cs` +```csharp +IsGuidBeam = _IODevice.ReadDO(17); // :270 DO 되읽기(피드백 아님) +IsLaserReady = _IODevice.ReadDI(11) && _IODevice.ReadDI(15); // :271 ★ 직접 판정 +... +_IODevice._iOUnit.WriteDoBit(9, true); // :557 Laser Ready 버튼 +_IODevice._iOUnit.WriteDoBit(13, true); +``` + +**출사 게이트** — 같은 파일 `:415` +```csharp +if (IsLaserReady == false) { MessageUtil.ShowWarningToast("Please check Laser Ready."); return; } +``` + +**여기서 얻을 것:** 레이저 제어가 `Laser` 를 안 거치고 **화면이 IO 보드를 직접 때린다.** +그래서 "Laser Ready 가 무엇인가" 의 정의가 화면 안에 들어가 버렸다. + +--- + +## 6. ⑤ 시퀀스 쪽은 더 애매하다 — `SeqComponentTeachingRun.cs` + +```csharp +// :473 진입부 — IO 가 아니라 통신형 Laser 디바이스에 물어본다 +var res = context.RetryUntilSuccess(_Laser.GetLaserReady, timeoutSeconds * 1000, 1); + +// :561 출사 직전 +var res = context.RetryUntilSuccess(LaserReadyResponse, timeoutSeconds * 1000, 10); + +// :658 그런데 그 함수는… +private bool LaserReadyResponse() +{ + //if (ApplicationExt.IsSimulation) + return true; // ★ 항상 통과 (본문 전부 주석) +} +``` + +**정리하면 Manual 출사 경로의 Ready 확인은 3계층인데:** + +| 계층 | 판정 | 효력 | +|---|---|---| +| 화면 게이트 `:415` | `DI11 && DI15` | **유일하게 실동작** | +| 시퀀스 진입 `:473` | 통신형 Laser 호출 | IO 결선과 무관 | +| 출사 직전 `:658` | `return true` | **인터락 없음** | + +→ **시퀀스 안에는 IO 기반 Ready 확인이 없다.** 실동작 인터락은 Head Theta(`:572~613`) 뿐. + +그리고 "Laser Ready" 정의가 소스에 **3벌**로 갈려 있다: + +| 정의처 | 핀 | enum 이름 | +|---|---|---| +| ViewModel `:271` | DI 11 && DI 15 | `Laser_Output_State` (= 출사 *중* 상태) | +| `LaserIOControl.cs:77` | DI 9 && DI 13 | `Laser_Ready` | +| 시퀀스 `:473` | (IO 미사용) | 통신형 | + +--- + +## 7. 여기서 헷갈리기 쉬운 지점 — 문제는 **두 개**다 + +``` +(A) 제어 경로가 분산됨 → 정의 3벌, 이력 추적 불가 → 해법: Laser 계층으로 일원화 +(B) 핀 번호가 코드에 박힘 → 배선 바뀌면 재빌드 → 해법: 핀 번호 외부화 +``` + +**둘은 독립이다.** +- (A) 만 고치면 → 구조는 깨끗해지지만 `LaserTrumpf_IO` 도 enum 하드코딩이라 **여전히 소스를 고쳐야 한다.** +- (B) 만 고치면 → 소스는 안 고쳐도 되지만 **정의는 계속 흩어져 있다.** + +이번 건에서 실제로 겪은 비용: `DI9` → `DI12 && DI24`(260617) → `DI11 && DI15`(260618) — **소스 3회 수정.** + +--- + +## 8. ⑥ (B)의 열쇠 — 이름으로 핀 찾기 + +```csharp +// AuroraDevices/AuroraIO/IOUnit.cs:367 +public int GetDiIndex(string id) +{ + foreach (InfoIO io in ListDI) + if (io.ID == id) return io.Index; + return -1; // 미등재 +} +``` + +`ListDI` 는 런타임에 **`DI.csv` 를 읽어** 채워진다(`IOUnitConfig.IOInterfaceMapDI` → `IODevice.cs:252`). + +→ 코드에 **번호 대신 이름**("LASER Ready")만 두면, 배선이 바뀌어도 **CSV 한 줄 수정으로 끝.** 재빌드 0. + +> **방증:** `DI.csv` / `DO.csv` 는 **이 저장소에 없다.** 원래부터 소스가 아니라 **현장 데이터**로 관리되도록 설계된 파일이다. +> 프레임워크는 "핀 번호는 데이터" 라고 이미 선언해 뒀는데, 우리가 그 자리를 안 쓰고 코드에 박은 것. + +**주의:** 기본 `DI.csv` 는 `12=START SWITCH / 24=FAN 8 ALARM / 25=LASER Ready` 로 **enum 과 이미 어긋나 있다.** +번호 확정 시 CSV 대조 필수. + +--- + +## 9. 이설(A공장→B공장) 관점 — 값의 3계층 + +| 계층 | 예시 | 있어야 할 곳 | 이설 시 | +|---|---|---|---| +| ① 설비 고유·불변 | 시퀀스 흐름, 안전 판정 **알고리즘**, PLC 규약 | **코드** | 안 바뀜 | +| ② 호기·현장 가변 | **IO 핀**, PLC IP/국번, 원점·오프셋, 경로, 타임아웃 | **설정(CSV/json)** | **여기만 교체** | +| ③ 생산 가변 | 파워·속도·좌표·페이지 | **레시피/모델** | 품종별 | + +**우리 문제:** ② 인 Laser Ready DI 가 ① 에 들어가 있다. + +**권장 형태** +``` +소스(불변) ─┐ + ├─ 실행 +현장 프로파일 ─┘ ← 이설 시 이 폴더만 교체 + ├ DI.csv / DO.csv, DeviceConfig, *Setting.json, 보정파일 +``` ++ 반드시 함께: **① 기동 시 매핑 검증·로그**(미등재 `-1` 이면 fail-closed·알람), **② 프로파일 형상관리**(공장/호기별 백업). + +**한계선:** 인터락 **판정 로직 자체**는 코드에 유지한다. +현장이 바꿀 수 있는 범위는 **"어느 핀이 그 신호인가" 까지**. 그 이상은 사고 시 책임 소재가 사라진다. + +**시뮬 주의:** `LaserTrumpf_IO.GetLaserReady()` 는 `_IsSimul` 이면 **false** 를 반환한다(`:273`). +IO형으로 전환하면 개발 PC 에서 출사가 막힐 수 있다 — 전환 시 정책을 정해야 한다. + +--- + +## 10. 오늘의 결론 3줄 + +1. **IO Device 는 IO Device 대로 두고, Laser Device 가 IO 에 의존(주입)** 하게 만들어 바깥에서는 `Laser` 만 호출 — 이게 Aurora 정석. +2. 지금 PTOP 방식도 **동작에는 지장 없다.** 비용은 6개월 뒤·다른 사람이 볼 때 발생한다(이력이 주석에만 남음). +3. **"매번 소스 수정" 은 계층이 아니라 핀 번호 외부화로 푼다.** 순서는 게이트 1곳 수렴 → CSV 라벨 조회 → 기동 검증 → (안정화 후) Laser 계층 흡수. + +--- + +## 11. 셀프 체크 (이해했는지 확인용) + +1. `Config.Type` 을 `Trumpf_IO` 로 바꾸면 무슨 일이 일어나나? 왜 그것만으로는 PTOP 에서 바로 안 되나? +2. 화면의 `IsLaserReady` 와 `LaserIOControl.GetLaserReady()` 는 왜 서로 다른 값을 낼 수 있나? +3. `LaserReadyResponse()` 를 지금 당장 실제 판정으로 되살리면 왜 위험한가? +4. 핀 번호를 CSV 로 뺐는데 CSV 에 이름이 없으면 어떻게 동작해야 하나? 왜? +5. 인터락 "허용 각도" 는 설정으로 빼도 되는데, "Front/Down 둘 다 아니면 차단" 규칙은 왜 코드에 둬야 하나? + +--- + +## 12. 남은 숙제 (확인 필요) + +- [ ] `CommonDeviceIds.IODevice` 실제 문자열이 `"IO_Board"` 인가 (`Laser.cs:93/97/101` 하드코딩 조회) +- [ ] PTOP 현재 `LaserConfig.Type` 설정값 +- [ ] `Trumpf_IO` 의 `ePTDIAddress/ePTDOAddress` 배치가 PTOP 실배선과 맞는가 (안 맞으면 PTOP 전용 IO형 구현체 필요) +- [ ] 현장 `DI.csv` 에 신규 Ready 신호가 **이름으로 등재**돼 있는가 → CSV 라벨 조회 가능 여부 +- [ ] 현장 신규 **Ready DI 번호 확정** (단일 신호인지 Core/Ring 2개 AND 인지) diff --git a/docs/분석/2026/07/28/ScannerCalibration_Index에러_및_VisionOffset_미입력_분석.md b/docs/분석/2026/07/28/ScannerCalibration_Index에러_및_VisionOffset_미입력_분석.md new file mode 100644 index 0000000..b9b0df6 --- /dev/null +++ b/docs/분석/2026/07/28/ScannerCalibration_Index에러_및_VisionOffset_미입력_분석.md @@ -0,0 +1,141 @@ +# Scanner Calibration — 그리드 마우스 조작 시 Index 에러 & One Step Vision Offset 미입력 분석 + +- 작성일: 2026-07-28 +- 대상: `UCScannerCalibration` (View / ViewModel / 코드비하인드) + `SeqScannerCalibration` +- 상태: **분석만 완료 (소스 미수정)** + +--- + +## 대상 파일 + +| 역할 | 경로 | +|---|---| +| View | `WeldingComponents/WeldingCommon/Views/Calibration/UCScannerCalibration.xaml` | +| 코드비하인드 | `WeldingComponents/WeldingCommon/Views/Calibration/UCScannerCalibration.xaml.cs` | +| ViewModel | `WeldingComponents/WeldingCommon/Views/Calibration/UCScannerCalibrationViewModel.cs` | +| 시퀀스 | `WeldingComponents/WeldingCommon/Sequences/Calibration/SeqScannerCalibration.cs` | + +--- + +## 이슈 1 — 그리드(row×column) 마우스 조작 중 Index 에러 + +### 증상 +Vision Correction List 그리드(row×column으로 채워진 셀)를 **마우스로 조작하다가 가끔 index 관련 에러**가 난다. + +### 근본 원인 — 방어 없는 `VisionCorrectionList[SelectedIndex]` 접근 + +핵심은 `MoveScanner()` (ViewModel `UCScannerCalibrationViewModel.cs:536`): + +```csharp +public void MoveScanner() +{ + double scannerPosX = VisionCorrectionList[SelectedIndex].MasterX; // ← 방어 없음 + double scannerPosY = VisionCorrectionList[SelectedIndex].MasterY; + ... +} +``` + +이 메서드는 그리드 **더블클릭** 시 호출된다 (`UCScannerCalibration.xaml.cs:149`): + +```csharp +private void GridEditorVisionCorrectionList_MouseDoubleClick(object sender, MouseButtonEventArgs e) +{ + _ViewModel.MoveScanner(); // 무조건 호출 — 유효 행 판정 없음 +} +``` + +`SelectedIndex`는 `SelectedCellsChanged`에서 `DataGrid.SelectedIndex`를 그대로 대입받는다 (`UCScannerCalibration.xaml.cs:108`): + +```csharp +_ViewModel.SelectedIndex = obj.SelectedIndex; // 선택 해제 시 -1 이 들어옴 +``` + +`DataGrid.SelectedIndex`는 **선택이 없으면 -1**을 반환한다. 따라서 다음 상황에서 `VisionCorrectionList[SelectedIndex]`가 `ArgumentOutOfRangeException`으로 터진다. + +#### 터지는 시나리오 + +1. **선택되지 않은 영역 더블클릭** (`SelectedIndex == -1`) + - 헤더, 마지막 행 아래 빈 공간, 빈 그리드를 더블클릭하면 `SelectedIndex`가 -1인 상태에서 `MoveScanner()` → `VisionCorrectionList[-1]` 예외. + - 특히 **Apply 버튼을 누르기 전**에는 리스트가 비어 있다. `InitVariable()`에서 `InitializeDataGridViewItem(...)` 호출이 주석 처리(`UCScannerCalibrationViewModel.cs:188`)돼 있어, 화면 진입 직후 `VisionCorrectionList.Count == 0`. 이때 더블클릭하면 `SelectedIndex`가 0이든 -1이든 무조건 out-of-range. + +2. **Esc → 더블클릭 조합** + - `KeyDown`에서 Esc를 누르면 `dg.SelectedItem = null` (`UCScannerCalibration.xaml.cs:140`) → `SelectedCellsChanged` 발생 → `SelectedIndex = -1`. 곧이어 더블클릭하면 예외. + +3. **화면 전환(IsVisibleChanged) 직후 stale 상태** + - `GridEditorVisionCorrectionList_IsVisibleChanged`에서 `UnselectAllCells()` (`UCScannerCalibration.xaml.cs:130`) → `SelectedIndex = -1`. + +4. **Apply 재적용으로 인한 stale 인덱스 (셀 개수 축소 시)** + - 큰 row×column으로 한 번 채운 뒤 큰 인덱스의 셀을 선택 → row/column을 줄여 다시 **Apply** → `InitializeDataGridViewItem`이 `VisionCorrectionList.Clear()` 후 더 적은 개수로 재생성(`UCScannerCalibrationViewModel.cs:510`). 그런데 `SelectedIndex`는 이전 큰 값 그대로 남아 있음 → 이후 더블클릭/시퀀스에서 `VisionCorrectionList[SelectedIndex]` out-of-range. + - Apply 시 `SelectedIndex`를 리셋하는 로직이 없다. + +### 이슈 1 권장 수정 (방향, 미적용) + +- `MoveScanner()` 진입부 가드: + ```csharp + if (SelectedIndex < 0 || SelectedIndex >= VisionCorrectionList.Count) return; + ``` +- `MouseDoubleClick` 핸들러에서 실제로 행(row)을 더블클릭했는지 확인 후 호출 (`e.OriginalSource`를 타고 `DataGridRow` 유무 확인, 또는 `dg.SelectedItem is CorrectionUnit` 체크). +- `PerformCmdApplyGridItems`(재적용) 시 `SelectedIndex = -1`로 리셋. +- `SelectedCellsChanged`에서 `-1` 방어(음수면 SelectedVisionCorrectionList 갱신만 하고 SelectedIndex는 별도 처리). + +--- + +## 이슈 2 — Scanner Cal 후 Vision Cal + One Step 이동/Find 했는데 Vision Offset(X,Y)이 안 들어감 + +### 증상 +Scanner Calibration → Vision Calibration 진행 후, 개별 **One Step**으로 선택 좌표 이동 → Find로 패턴을 찾는데, 결국 **Vision Offset 값이 안 들어가는 경우**가 있다. + +### 흐름 요약 + +- One Step 실행 시 `oneStepCalibrationIndex = SelectedIndex` 로 넘어감 (`UCScannerCalibrationViewModel.cs:474`). +- 시퀀스 `InitSequenceVariable`: `if (_InitParam.Is1StepCalibration) _PositionIndex = _InitParam.oneStepCalibrationIndex;` (`SeqScannerCalibration.cs:151`). +- Find 성공 시 `SetVisionPosition`이 이벤트로 `UpdateDataGridItem(_PositionIndex, visionBitX, visionBitY)` 발행 (`SeqScannerCalibration.cs:425`). +- ViewModel `OnUpdateCorrectionTable`가 받아서 그리드에 반영 (`UCScannerCalibrationViewModel.cs:223`): + ```csharp + VisionCorrectionList[updateDataGridItem.Upositionindex].VisionX = updateDataGridItem.UVisionX; + VisionCorrectionList[updateDataGridItem.Upositionindex].VisionY = updateDataGridItem.UVisionY; + ``` + +### 원인 후보 (값이 0으로 남거나 아예 반영 안 되는 경로) + +#### (A) 패턴 Find 실패 / 오프셋 미수렴 → **의도적으로 0,0 기록** +- `DecisionFindResult`에서 `_PatternResult.Status != FoundOK` 면 `SEQ_SET_VISION_POSITION_FAILED` (`SeqScannerCalibration.cs:350`). +- `DecisionCheckOffset`에서 10회(`_CheckOffsetCount < 10`) 내에 오프셋이 `_InitParam.CheckOffset`(= UI "Align Distortion", 기본 **0.01mm**) 아래로 못 내려가면 `SetVisionPositionFailed` (`SeqScannerCalibration.cs:405`). +- `SetVisionPositionFailed`는 `VisionX=0, VisionY=0`을 기록(`SeqScannerCalibration.cs:449`). +- → 사용자 눈에는 **"오프셋이 안 들어감(0으로 남음)"** 으로 보임. Align Distortion 임계값이 과도하게 타이트하면 정상 Find여도 수렴 실패로 0 처리됨. **가장 흔한 실사용 원인.** + +#### (B) `SelectedIndex == -1` 인 채 One Step 실행 → 시퀀스 예외로 미반영 +- One Step 체크했지만 그리드에서 **행을 선택하지 않았거나 Esc/화면전환으로 선택 해제**된 상태면 `SelectedIndex = -1`. +- `_PositionIndex = -1` → `CaptureNextPosition`의 `_MarkingPositionList_MM[-1]` (`SeqScannerCalibration.cs:282`) 또는 `OnUpdateCorrectionTable`의 `VisionCorrectionList[-1]`에서 예외 → 해당 셀 오프셋 미반영. +- `PerformCmdRunScannerCalibration`에는 `VisionCorrectionList.Count <= 0` 체크만 있고 **SelectedIndex 유효성 검사가 없음** (`UCScannerCalibrationViewModel.cs:445`). + +#### (C) `OnUpdateCorrectionTable`의 무방어 인덱스 접근 → 예외 시 조용히 누락 +- `VisionCorrectionList[Upositionindex]`에 범위/유효성 검사 없음 (`UCScannerCalibrationViewModel.cs:223`). +- 이벤트 처리 도중 사용자가 **Apply를 다시 눌러 리스트를 Clear/재생성**했거나 인덱스가 어긋나면 dispatcher 핸들러에서 예외 → 오프셋 반영 실패. try/catch도 없어 이후 업데이트에도 영향 가능. + +#### (D) 이벤트 반영 타이밍 +- 시퀀스는 `Publish` 후 `Thread.Sleep(100)`만 주고 진행 (`SeqScannerCalibration.cs:427`). 핸들러는 `.ObserveOnDispatcher()`로 UI 스레드에 큐잉되는데, One Step은 반영 직후 바로 `SEQ_SAVE_RESULT_DATA`로 종료(`DecisionCheckNextPostionExists`, `SeqScannerCalibration.cs:466`)된다. UI가 바쁘면 100ms 내 dispatcher 처리가 밀릴 수 있음(주 원인은 아니나 (C)와 겹치면 악화). + +### 참고 — 인덱스 매핑 자체는 일치 +UI `InitializeDataGridViewItem`(`UCScannerCalibrationViewModel.cs:512`)와 시퀀스 `InitSequenceVariable`(`SeqScannerCalibration.cs:163`) 모두 **column 바깥 / row 안쪽** 루프로 `index = column * rowCount + row` 동일. 즉 row/column count가 같으면 인덱스는 어긋나지 않는다. (단, count가 UI/시퀀스 간 달라지는 재적용 타이밍이 있으면 (C)로 이어짐.) + +### 이슈 2 권장 수정 (방향, 미적용) + +1. **One Step 실행 가드**: `PerformCmdRunScannerCalibration`에서 `Is1StepCalibration && (SelectedIndex < 0 || SelectedIndex >= VisionCorrectionList.Count)`면 경고 후 return. +2. **`OnUpdateCorrectionTable` 방어**: `Upositionindex` 범위 검사 + try/catch, 범위 밖이면 로그만. +3. **Find 실패/미수렴 가시화**: `SetVisionPositionFailed` 진입 시 사용자에게 명확히 알림(현재는 0,0만 조용히 기록). Align Distortion(CheckOffset) 기본값·현장값 재검토(0.01mm가 현실적으로 수렴 가능한지). +4. **`_CheckOffsetCount < 10` / CheckOffset 조합**을 현장 스캐너 정밀도에 맞게 튜닝. + +--- + +## 우선순위 제안 + +| 순위 | 대상 | 조치 | +|---|---|---| +| 1 | `MoveScanner()` / `OnUpdateCorrectionTable` | 인덱스 범위 가드 추가 (이슈1·2 크래시 즉시 차단) | +| 2 | `PerformCmdRunScannerCalibration` | One Step 시 SelectedIndex 유효성 검사 | +| 3 | `PerformCmdApplyGridItems` | 재적용 시 SelectedIndex 리셋 | +| 4 | `MouseDoubleClick` | 실제 DataGridRow 더블클릭만 MoveScanner 호출 | +| 5 | Align Distortion(CheckOffset) | 현장 임계값 재검토 + Find 실패 알림 | + +> 본 문서는 분석 전용이며 소스는 아직 수정하지 않았습니다. diff --git a/docs/작업일지/2026_07_23.md b/docs/작업일지/2026_07_23.md new file mode 100644 index 0000000..fb4241f --- /dev/null +++ b/docs/작업일지/2026_07_23.md @@ -0,0 +1,162 @@ +# 작업일지 — 2026-07-23 + +## 1. Recipe Laser Shot — Laser Ready / Guide Beam IO 번호 확인 및 상시 ON 레이저 대응 + +### 배경 +현장 레이저가 기존과 달라 배선을 다른 곳에 물렸고, **레이저 컨트롤러를 상시 ON** 상태로 둔 채 그 값을 읽어 +**TRUE로 유지돼야 레시피 출사 시퀀스가 동작**하는 구조로 가야 함. +Recipe의 Laser Shot 구간에서 Laser Ready 버튼 / Guide Beam 버튼이 물린 **IO 번호 확인** 요청. + +> **현재 프로그램 SOURCE는 수정하지 않음** — 분석 및 수정안(DIFF)만 기록. + +### 확인 결과 — 현재 사용 중인 IO 번호 +| 항목 | 방향/번호 | enum 의미 | 소스 위치 | +|---|---|---|---| +| Laser Ready 판정(모니터링) | **DI 11 && DI 15** | `CoreLaser_Laser_Output_State` / `RingLaser_Laser_Output_State` | `UCScannerObjectTeachingControlBoxViewModel.cs:271` | +| Laser Ready 버튼(토글 출력) | **DO 9, DO 13** | `CoreLaser_Control` / `RingLaser_Control` | 같은 파일 `:552~567` | +| Guide Beam 상태 표시 | **DO 17 (읽기)** | `EX_Red_Control` | 같은 파일 `:270` | +| Guide Beam 버튼(토글) | **DO 17 (쓰기)** | `EX_Red_Control` | 같은 파일 `:534~542` | +| 출사(TeachingRun) 게이트 | `IsLaserReady == false` → 차단 | — | 같은 파일 `:415~418` | + +- Guide Beam은 DI가 없어 **DO 17 명령 비트를 되읽어 표시**(실제 빔 출력 피드백 아님). +- Laser Ready 판정 IO 변천: `DI 9` → `DI 12 && DI 24`(260617) → `DI 11 && DI 15`(260618, 현재). + +### 현재 구조가 상시 ON 레이저와 안 맞는 이유 +- DO9/DO13 토글은 더 이상 Ready 성립 조건이 아님(안 눌러도 Ready여야 하고, 눌러도 무의미). +- 판정은 **새로 물린 DI를 상시 읽어 TRUE 유지**하는 형태여야 함. +- 변경 포인트는 **271행 판정식(필수)** + **버튼 의미 재정의(선택)** 두 곳뿐. + +### 작성한 수정안 (미적용) +- 판정 DI를 파일 상단 상수(`DI_LASER_READY_MAIN` / `DI_LASER_READY_SUB`)로 추출 → 현장 번호 확정 시 상수만 교체. + `SUB < 0` 이면 단일 DI 판정으로 자동 동작. +- 점검용 `FORCE_LASER_READY`(기본 false, true 커밋 금지) 추가. +- Laser Ready 버튼: DO9/13 쓰기 차단 + 현재 DI 상태 토스트 안내(구 레이저 복귀 대비 주석 보존). +- 출사 게이트(`PerformCmdRunTeachingRun`)는 변경 없음 — `IsLaserReady`가 TRUE 유지되면 자동 통과. +- (별건) `LaserIOControl.cs` `RingLaser_Control_Status = 23` → 실배선 기준 `24` 정정 권장. + +### 주의 — DI/DO 실제 의미 +인덱스의 실제 의미는 enum이 아니라 **런타임 로드되는 `DI.csv`/`DO.csv` 맵**이 결정한다 +(Setting → Device(IODevice) Config의 `IOInterfaceMapDI/DO`, 소스 하드코딩 없음). +기본 `DI.csv`는 12=START SWITCH, 24=FAN 8 ALARM, 25=LASER Ready 로 **enum과 불일치 가능** → 번호 확정 시 CSV 대조 필수. + +### 잔여·후속 작업 +- [ ] 현장 신규 배선 **Ready DI 번호 확정**(단일인지 Core/Ring 2개 AND인지 포함) +- [ ] 로드 중인 `DI.csv`에서 해당 번호 라벨 확인 +- [ ] 판정부 적용 + 상수값 교체 → Recipe 화면에서 컨트롤러 ON만으로 Ready=TRUE 확인 +- [ ] TeachingRun(출사)이 버튼 조작 없이 진입되는지 확인 +- [ ] Guide Beam은 여전히 명령 비트 되읽기 — 신규 레이저에 가이드빔 제어선 별도 존재 시 DO 번호 재확인 + (정식 경로 TruPulse `ExtPilotOnOff` 미구현) + +**상세 분석서:** `docs/분석/2026/07/23/Laser_Shot_IO_LaserReady_GuideBeam_분석.md` + +--- + +## 2. 시퀀스 측 레이저(IO) 인터락 추가 분석 — `SeqComponentTeachingRun` + +Manual 출사를 `SeqComponentTeachingRun` 으로 돌릴 예정이므로, 시퀀스 안의 레이저 Ready 확인부까지 확인. +**소스는 여전히 미수정(분석만).** + +### 확인 결과 — Ready 를 보는 곳은 3계층, 그중 실효는 1개뿐 +| 계층 | 위치 | 판정 | 효력 | +|---|---|---|---| +| ① 화면 게이트 | ViewModel `:415` ← `:271` `DI11 && DI15` | IO 기반 | **유일한 실동작 IO 인터락**(버튼 클릭 시점 1회) | +| ② 시퀀스 진입 | `SeqComponentTeachingRun.cs:473` `_Laser.GetLaserReady` | Aurora Laser 디바이스(**IO 아님**) | 결선과 무관 | +| ③ 출사 직전 | 같은 파일 `:561` → `LaserReadyResponse()` `:658` | `return true;` 스텁 | **인터락 없음** | + +→ **시퀀스 내부에는 IO(DI) 기반 Ready 확인이 없다.** 실동작 인터락은 Head Theta(`:572~613`)뿐. + +### "Laser Ready" 정의가 3벌로 갈려 있음 +- ViewModel `:271` = **DI 11 && DI 15**(`Laser_Output_State`) +- `LaserIOControl.cs:77` = **DI 9 && DI 13**(`Laser_Ready`) +- 시퀀스 `:473` = 레이저 디바이스 호출(IO 미사용) + +### 부수 발견 +- `LaserIOControl` 은 PTOP 에서 dead — IODevice 는 `PAIX_PTOP` 등록(`EquipmentSetup.cs:153`), + `SeqTestRun.cs:469` 의 `as LaserIOControl` 는 항상 null. **단 `SeqTestRun` 은 미사용 파일(확인 완료)** → 실동작 리스크 없음. +- **Auto 경로엔 레이저 Ready 인터락 자체 없음** — `SeqAutoRunPTOP.cs:589` 는 no-op 이고 호출처도 없음. +- `ePrismaticDIAddress/DOAddress` enum 이 `PAIX_PTOP.cs` / `LaserIOControl.cs` **2벌 중복**. + +### 수정 방향(미적용) — 판정 1곳 + 호출 3곳 +1. 공용 게이트 `WeldingCommon/Devices/Laser/LaserReadyGate.cs` 신설(상수 MAIN/SUB/FORCE, `IsReady(IODevice)` fail-closed) + — 구식 csproj 라 `` 수동 추가 필요 +2. ViewModel `:271` → 게이트 호출 +3. 시퀀스 `:473` → `_Laser.GetLaserReady` 대신 게이트 +4. 시퀀스 `:658 LaserReadyResponse()` → 게이트 (출사 직전 재확인 부활) +5. `LaserIOControl.GetLaserReady()` 도 게이트로 위임 +6. Laser Ready 버튼(DO9/13) 쓰기 차단 + DI 상태 토스트 + +**리스크:** 4번은 "항상 통과 → 미충족 시 차단"으로 바뀌므로 **Ready DI 번호 확정 후 마지막에** 적용. +Auto 경로 게이트 추가는 양산 정지 리스크가 새로 생기므로 현장 합의 후 별도 결정. + +**상세:** 같은 분석서 §6~§9 + +--- + +## 3. 아키텍처 검토 — IO 직접 제어 vs Laser 추상화 계층 (학습용) + +**소스 수정 없음. 개념 정리 + 다음 프로젝트 방향성 기록.** + +### 논의 +- **차장님 의견:** 추상화 레벨이 `Laser` 클래스이므로, **디바이스 등록 시점에 IO 보드를 Laser 에 물리고 IO 주소/비트 맵도 함께 등록**해 한 곳에서 관리했어야 함. +- **PTOP 현실:** IO 는 `IODevice` 로 따로 등록 → 화면·시퀀스가 address 를 직접 읽어 개별 제어. +- **결론(받은 답변):** IO Device 는 IO Device 대로 쓰되 **Laser Device 가 IO Device 에 의존적으로 동작**하는 게 정석. **이력을 모르면 추적이 어려울 뿐, 사용에는 지장 없음.** + +### 확인한 사실 +- Aurora 에 **IO형 레이저 구현이 이미 존재** — `Laser.cs:90~103` 의 `eLaserType.IPG_YLS / Coherent_Arm_IO / Trumpf_IO` 가 `DeviceManager["IO_Board"]` 를 꺼내 `ILaser` 구현체에 **주입**. +- `LaserTrumpf_IO` 는 `LaserOn/Off`, `GetLaserReady`, `PilotLaser`, 300ms 상태 폴링·램프 갱신까지 **전부 DI/DO 로 구현**돼 있음 → 지금 ViewModel 이 손으로 하는 일이 이미 들어있음. +- **단, 계층을 옮겨도 "매번 소스 수정"은 안 없어짐** — `LaserTrumpf_IO` 도 핀이 `ePTDIAddress` enum 하드코딩. + → 문제는 **(A) 제어 경로 분산** 과 **(B) 핀 번호 하드코딩** 2개이며 서로 독립. + +### (B) 해결 수단 — 이미 사용 가능 +- **옵션 1(추천, 재빌드 0):** `IOUnit.cs:367 GetDiIndex(string id)` / `:380 GetDoIndex` 로 **`DI.csv` 라벨 조회** → 배선 변경 시 CSV 한 줄만 수정. +- **옵션 2:** `LaserInterlockSetting` 에 DI 번호 필드 추가 → 세팅 화면에서 변경(재빌드 없음). +- **옵션 3(중장기):** `LaserConfig` 확장 + IO형 일원화 — 공용 `AuroraLaser` 수정이라 타 장비 회귀 리스크. + +### 다음 방향성 +즉효로 옵션 1/2 + 판정 게이트 1곳 수렴 → 안정화 후 게이트 내부를 `_Laser.GetLaserReady()` 위임으로 교체해 Laser 계층에 흡수. +신규 프로젝트는 **처음부터 `eLaserType.*_IO` 등록 + 핀 맵 Config/CSV 관리**. + +### 숙제 +- `CommonDeviceIds.IODevice` 실제 값이 `"IO_Board"` 인지 (`Laser.cs` 가 문자열 하드코딩 조회 → 다르면 IO형 전환 시 NRE) +- PTOP 현재 `LaserConfig.Type` 설정값 / `Trumpf_IO` 핀 배치와 실배선 일치 여부 +- `DI.csv` 에 신규 Ready 신호가 이름으로 등재됐는지(옵션 1 가능 여부) +- `LaserTrumpf_IO.GetLaserReady()` 는 시뮬에서 **false** 반환(`:273`) → 개발 PC 출사 막힘, 전환 시 정책 확인 + +**상세:** 같은 분석서 §10~§17 + +--- + +## 4. 이설·다현장 대응 관점 — "왜 설정화인가" + +**질문:** A공장에 맞춰 놓아도 B공장으로 이설하면 바뀌는 부분이 있는데, 소스 수정으로 대응하는 게 맞나? +**답:** 맞지 않다. 단 "전부 설정화"도 아니고 **값의 종속성으로 3계층 분리**가 먼저. + +| 계층 | 내용 | 위치 | 이설 시 | +|---|---|---|---| +| ① 설비 고유·불변 | 시퀀스 흐름, 안전 판정 **알고리즘**, PLC 규약 | 코드 | 안 바뀜 | +| ② 호기·현장 가변 | **IO 핀 매핑**, PLC IP/국번, 원점·오프셋, 경로, 타임아웃 | 설정(CSV/json) | **여기만 교체** | +| ③ 생산 가변 | 파워·속도·좌표·페이지 | 레시피/모델 | 품종별 | + +- **현 문제:** ②인 Laser Ready DI 가 ①(코드)에 있음 → 레이저 교체만으로 재빌드. 실제 이번 건에서 `DI9 → DI12&24 → DI11&15` **소스 3회 수정.** +- **Aurora 는 ② 자리를 이미 제공** — `IOUnitConfig.IOInterfaceMapDI/DO`, `IODevice.cs:252` CSV 로드, `IOUnit.cs:367 GetDiIndex(string)`. + **`DI.csv` 가 저장소에 없다는 것 자체가 "현장 데이터로 관리하라"는 설계 방증.** +- **권장:** 소스 / **현장 프로파일**(DI·DO CSV, DeviceConfig, Setting json, 보정파일) 분리 → 이설 시 프로파일만 교체. + 단 **① 기동 시 매핑 검증+로그**(미등재 `-1` 이면 fail-closed·알람), **② 프로파일 형상관리(공장/호기별 백업)** 를 반드시 같이 넣을 것. +- **한계선:** 인터락 **판정 로직 자체**는 코드에 유지(현장이 바꿀 수 있는 건 "어느 핀인가"까지). 설정 변경은 권한 제한 + 이력 로그. +- **진행 순서:** 게이트 1곳 수렴 → CSV 라벨 조회 → (대체: Setting 노출) → 기동 검증·로그 → 안정화 후 Laser 계층 위임. + +**상세:** 같은 분석서 §18~§23 + +--- + +## 5. 학습노트 (읽기용 정리본) + +소스를 열지 않고도 흐름이 잡히도록 **핵심 코드를 옮겨 담은 정리본**을 별도 작성. + +**`docs/분석/2026/07/23/레이저제어_구조_학습노트.md`** + +- §0 30초 요약 → §1 Aurora 레이저 계층도 → **§2 코드 읽는 순서(①~⑥)** +- §3 `LaserTrumpf_IO` 가 DI/DO 로 하는 일(코드 발췌) → §4 구현체가 갈리는 지점(`Laser.cs:60~103`) +- §5 PTOP 실제 등록/화면 코드 → §6 시퀀스 Ready 3계층 + 정의 3벌 표 +- §7 문제는 두 개(A 경로 분산 / B 핀 하드코딩, 서로 독립) → §8 `IOUnit.GetDiIndex` 로 이름 조회 +- §9 이설 관점 3계층 + 현장 프로파일 → §10 결론 3줄 → **§11 셀프 체크 5문항** → §12 숙제