LGES-OC5-PtoP-Welder/docs/분석/2026/07/23/레이저제어_구조_학습노트.md
2026-07-28 17:09:48 +09:00

13 KiB

학습노트 — 레이저 제어 구조 (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 쓰기로 구현해 놓았다.

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

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

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

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

if (IsLaserReady == false) { MessageUtil.ShowWarningToast("Please check Laser Ready."); return; }

여기서 얻을 것: 레이저 제어가 Laser 를 안 거치고 화면이 IO 보드를 직접 때린다. 그래서 "Laser Ready 가 무엇인가" 의 정의가 화면 안에 들어가 버렸다.


6. ⑤ 시퀀스 쪽은 더 애매하다 — SeqComponentTeachingRun.cs

// :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)의 열쇠 — 이름으로 핀 찾기

// 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 인지)