# 학습노트 — 레이저 제어 구조 (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 인지)