13 KiB
학습노트 — 레이저 제어 구조 (IO 직접 제어 vs Laser 추상화)
작성: 2026-07-23
용도: 혼자 읽으며 공부하기 위한 정리본. 소스를 안 열어도 흐름이 잡히도록 핵심 코드를 여기 옮겨 놓았다.
원본 분석서: Laser_Shot_IO_LaserReady_GuideBeam_분석.md (§1~§23) — 근거·표·체크리스트는 그쪽에 있음
0. 30초 요약
- Aurora 에는
Laser라는 추상화 계층이 있고, 그 아래ILaser구현체가 통신형/IO형으로 갈린다. - IO 보드로 레이저를 제어하는 구현체도 이미 있다(
LaserTrumpf_IO등). IO 보드를 Laser 에 주입하는 구조. - PTOP 는 그 계층을 안 쓰고, 화면·시퀀스가 IO 보드를 직접 읽어 레이저를 제어한다.
- 동작에는 문제없다. 문제는 Ready 정의가 3벌로 흩어지고, 배선 변경 이력이 코드에 안 남는 것.
- 그리고 "배선 바뀔 때마다 소스 수정" 은 계층 문제가 아니라 핀 번호가 코드에 박혀 있어서다. 둘은 별개 문제.
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 대기·타임아웃·시뮬 처리 같은 정형 패턴이 한 곳에 응집돼 있다.
- 다만 핀 번호는
ePTDIAddressenum 하드코딩 — 이게 §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줄
- IO Device 는 IO Device 대로 두고, Laser Device 가 IO 에 의존(주입) 하게 만들어 바깥에서는
Laser만 호출 — 이게 Aurora 정석. - 지금 PTOP 방식도 동작에는 지장 없다. 비용은 6개월 뒤·다른 사람이 볼 때 발생한다(이력이 주석에만 남음).
- "매번 소스 수정" 은 계층이 아니라 핀 번호 외부화로 푼다. 순서는 게이트 1곳 수렴 → CSV 라벨 조회 → 기동 검증 → (안정화 후) Laser 계층 흡수.
11. 셀프 체크 (이해했는지 확인용)
Config.Type을Trumpf_IO로 바꾸면 무슨 일이 일어나나? 왜 그것만으로는 PTOP 에서 바로 안 되나?- 화면의
IsLaserReady와LaserIOControl.GetLaserReady()는 왜 서로 다른 값을 낼 수 있나? LaserReadyResponse()를 지금 당장 실제 판정으로 되살리면 왜 위험한가?- 핀 번호를 CSV 로 뺐는데 CSV 에 이름이 없으면 어떻게 동작해야 하나? 왜?
- 인터락 "허용 각도" 는 설정으로 빼도 되는데, "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 인지)