feat :[문서] 가접용접_메뉴얼 추가

This commit is contained in:
노도연 2026-06-26 15:13:52 +09:00
parent 9c1daaee1d
commit c63a228dad
4 changed files with 369 additions and 0 deletions

View File

@ -0,0 +1,139 @@
# 알루미늄 모서리 휨 / 시작·끝점 어긋남 분석 — Jump/Mark/Polygon Delay 관점
- 작성일: 2026-06-25
- 대상: `WeldingComponents/WeldingCommon/Sequences/Main/SeqUploadWeldingList.cs`
- 증상: ㄱ 자 형태(Line #1 + Line #2)로 Draw 했을 때, **모서리(꼭짓점) 부근 시작점/끝점이 Draw 위치와 맞지 않고**, 알루미늄에서 모서리가 휘는(열변형) 현상
---
## 1. 결론 요약
ㄱ 자 코너에서 발생하는 두 가지 증상은 서로 다른 원인이지만 모두 **스캐너 딜레이(Delay) 파라미터**와 직결된다.
| 증상 | 1차 원인 파라미터 | 메커니즘 |
|------|------------------|----------|
| 시작/끝점이 Draw 위치와 어긋남(빔이 도형 밖/안에 찍힘) | `JumpDelay`, `MarkDelay`(+ `JumpSpeed`/`MarkSpeed`) | 갈바노 미러 **정정(settling) 부족** → 명령 위치보다 실제 빔이 뒤처짐(tracking error) |
| 코너가 둥글게 깎이거나(rounding) 위치가 안 맞음 | `PolygonDelay`(= arcDelay) | 두 Line이 만나는 꼭짓점 통과 시 미러가 꼭짓점에 **도달하기 전에 다음 벡터로 전환** → 코너 컷 |
| 코너에서 알루미늄이 휨(열변형) | `PolygonDelay`(과대), `MarkDelay`(과대), `LaserOffDelay` | 코너에서 **체류(dwell) 시간 증가 → 국부 입열 집중** → 고반사·저융점 알루미늄 모서리 열변형 |
| 끝점이 Draw보다 길게 타거나 끝단 변형 | `LaserOffDelay` (현재 **버그로 미동작 가능**) | 마크 종료 후 레이저 Off가 지연되어야 하는데, Power=0 선기록으로 Off Delay가 무시됨 |
핵심: **알루미늄은 고반사·고열전도·저융점**이라 동일 딜레이라도 강재 대비 코너 입열 민감도가 훨씬 크다. 코너 체류시간(딜레이)을 강재 레시피 그대로 쓰면 모서리가 휜다.
---
## 2. 딜레이 파라미터의 물리적 의미와 단위
`SetScannerProcessCondition()`에서 매 Object마다 설정된다. (`SeqUploadWeldingList.cs:632-636`)
```csharp
// 1.레이저 ON/OFF delay 설정
_Scanner.SetLaesrDelay(param.LaserOnDelay, param.LaserOffDelay);
// 2.Mark delay 설정
_Scanner.SetScannerDelay(param.JumpDelay, param.MarkDelay, param.PolygonDelay);
```
RTC6 드라이버 변환 (`AuroraDevices/AuroraLaserScanner/ScannerRTC6.cs:691-700`)
```csharp
public override void SetLaesrDelay(int laserOnDelay, uint laserOffDelay) {
const double delayScale = 0.5; // 1bit당 0.5us
RTC6Wrap.n_set_laser_delays(m_deviceNo, Convert.ToInt32(laserOnDelay / delayScale),
Convert.ToUInt32(laserOffDelay / delayScale));
}
public override void SetScannerDelay(uint jumpDelay, uint markDelay, uint arcDelay) {
const double delayScale = 10; // 1bit당 10us
RTC6Wrap.n_set_scanner_delays(m_deviceNo, jumpDelay/delayScale, markDelay/delayScale, arcDelay/delayScale);
}
```
| 파라미터 | 타입 | 의미 | 코너/끝점에 미치는 영향 |
|----------|------|------|----------------------|
| `JumpDelay` | uint | Jump(레이저 OFF 이동) 후 다음 명령까지 대기 | 코너 이전 **점프 후 정정 시간**. 작으면 미러 미정정 상태로 마크 시작 → **Line #1 시작점이 Draw와 어긋남** |
| `MarkDelay` | uint | Mark(레이저 ON 이동) 종료 후 대기 | Line #1 끝(=코너) 도달 후 정정 시간. 작으면 **끝점 미달(코너 못 미침)**, 크면 끝점 **체류 입열↑** |
| `PolygonDelay` | uint | 벡터(꺾임) 사이 전환 대기 = arcDelay | **ㄱ 코너 그 자체의 파라미터**. 작으면 코너 rounding, 크면 코너 dwell→알루미늄 휨 |
| `LaserOnDelay` | **int(부호 가능)** | 이동 시작 대비 레이저 ON 시점 | 양수: 시작점 과입열 방지. 너무 크면 시작 구간 미용접 |
| `LaserOffDelay` | uint | 이동 종료 대비 레이저 OFF 지연 | 끝점 over-burn 제어. **현재 코드상 미동작 의심(§4)** |
> 단위 주의: Jump/Mark/Polygon은 **10us/bit**, Laser On/Off는 **0.5us/bit**. UI 입력값이 us 단위라면 코드에서 `/delayScale`로 bit 변환한다.
---
## 3. ㄱ 자(Line #1 + Line #2) 업로드 시 실제 명령 흐름
Wobble/PowerProfile/Ramping 미사용의 일반 라인은 `LoadLineScannerParam()`의 마지막 else 분기로 진입한다. (`SeqUploadWeldingList.cs:861-978`)
각 Line은 `FuncLoadDrawObjectScannerParamSelected()`에서 **개별 Object로 순차 처리**되며, 두 Line의 연결부 동작은 `_LastX/_LastY` 비교로 결정된다.
### Line #1 처리
1. `SetScannerProcessCondition()` → 딜레이/파워/속도 전부 재설정
2. `JumpAbs(PF1)` — 시작점으로 점프 (직전 위치와 0.02mm 이내면 점프 생략, `:868`)
3. `WriteDA(percentLaser1/2)` → `MarkAbs(PF2)` — 끝점(코너 꼭짓점)까지 마크 (`:968-970`)
4. `_LastX/_LastY = PF2` 갱신 (`:980-981`)
### Line #2 처리
1. `SetScannerProcessCondition()` 재호출 — **딜레이/파워/속도 다시 set, WriteDA, SetMarkSpeed 등 list 명령 삽입**
2. `_LastX/_LastY`(=Line#1 끝점) vs `Line#2.PF1` 비교
- **두 점이 같으면(코너 공유) → 점프 생략, 연속 마크** (`:868`)
3. `MarkAbs(PF2)` — Line #2 끝점까지 마크
### 코너(꼭짓점)에서 일어나는 일
- Line #1 끝점 == Line #2 시작점이면 **점프 없이 두 마크 벡터가 직접 연결**되고, RTC는 이 꺾임 지점에 **자동으로 `PolygonDelay`(arcDelay)를 적용**한다. → **PolygonDelay가 ㄱ 코너 형상을 좌우하는 핵심 파라미터.**
- 단, 두 Line 사이에 `SetScannerProcessCondition()`이 다시 호출되어 `WriteDA`/`SetMarkSpeed`/`SetWobbelMode` 등 list 명령이 끼어든다. 코너 직전에 파워·속도가 바뀌면 코너 입열 프로파일이 불연속이 되어 **모서리 변형/위치 어긋남을 키울 수 있다.**
> 의심 포인트: Draw 상에서는 Line #1 끝과 Line #2 시작이 정확히 같은 좌표여야 코너가 연속 처리된다. **두 점이 0.02mm를 초과해 어긋나 있으면** `JumpAbs`가 끼어들어 코너에서 레이저 OFF→재점프→재시작이 발생하고, JumpDelay 정정 부족 시 **재시작점이 Draw 꼭짓점과 어긋난다.** Teaching 좌표 일치 여부를 먼저 확인할 것.
---
## 4. ⚠️ LaserOffDelay 미동작 의심 (소스 명시 #warning)
`:872`에 개발자가 남긴 경고가 있다.
```csharp
#warning 251211 Laser Off Delay가 다 수행되기 전에 power를 0으로 해버려서, Laser OffDelay 동작을 안하는 현상
//_Scanner.WriteDA((uint)eAnalogPortOut.DA1, 0, eFunctionType.LIST);
//_Scanner.WriteDA((uint)eAnalogPortOut.DA2, 0, eFunctionType.LIST);
_Scanner.JumpAbs(obj.PF1.X + xOffset, obj.PF1.Y + yOffset, eFunctionType.LIST);
```
- Jump 직전에 `WriteDA(0)`(파워 0)을 주면, `LaserOffDelay`가 끝나기 전에 파워가 0이 되어 **Off Delay가 사실상 무시**된다.
- 그래서 위 두 줄을 **주석 처리**해 둔 상태다. 즉 현재는 **Jump 시 파워를 0으로 끄지 않는다.**
- 영향:
- (주석 유지 시) 끝점/코너에서 Off Delay는 살지만, **점프 구간에서 파워가 0이 아니므로 이동 중 잔류 출사** 가능 → 코너 인접부 의도치 않은 입열 → **알루미늄 모서리 열변형의 직접 원인 후보.**
- (주석 해제 시) Off Delay 무시 → 끝점 처리가 의도와 달라짐.
- → LaserOffDelay와 끝점/코너 변형이 의심되면 **이 분기 동작을 우선 검증**해야 한다. (Dot 분기 `:1003-1004`는 반대로 Jump 전 `WriteDA(0)`를 정상 수행 — 라인과 동작이 다름.)
---
## 5. 알루미늄 특이성 — 왜 강재 레시피로 쓰면 휘나
- **고반사(>90%)**: 시작점에서 흡수율이 낮아 LaserOnDelay/시작 파워가 부족하면 미입열, 안정화되면 급격히 흡수↑ → 시작점 불안정.
- **고열전도**: 코너에서 PolygonDelay/MarkDelay로 인한 체류가 짧아도 주변으로 열이 빠르게 퍼져 **모서리 전체가 변형**.
- **저융점·저강성**: 국부 입열 집중 시 박판이 쉽게 휨(buckling). 코너는 두 라인의 입열이 겹치는 지점이라 누적 입열이 최대 → **가장 먼저 휜다.**
- 결론: 알루미늄은 **코너 체류시간(Polygon/Mark Delay)을 강재보다 작게**, 동시에 미러 정정을 위한 최소 딜레이는 확보하는 **타이트한 튜닝 범위**를 요구한다. 딜레이를 키워 위치 정확도를 확보하면 입열↑로 휨, 줄이면 코너 rounding — 트레이드오프.
---
## 6. 점검·튜닝 권고 (현장 우선순위)
1. **Teaching 좌표 일치 확인**: Line #1 끝 == Line #2 시작인지(0.02mm 이내). 어긋나면 코너에서 불필요한 Jump 발생 → 우선 해결.
2. **`PolygonDelay` 우선 조정**: 코너 형상의 1차 인자.
- 코너가 둥글게 깎임 → PolygonDelay 증가(미러 정정 확보)
- 코너가 휨/과입열 → PolygonDelay 감소(체류 입열 저감)
3. **`MarkDelay` / `JumpDelay`**: 시작·끝점이 Draw와 어긋나면(미달/오버) 증가. 단 알루미늄은 MarkDelay 과대 시 끝점 입열↑ 주의.
4. **`MarkSpeed`와 동반 검토**: 딜레이는 속도와 같이 봐야 한다. 속도↑면 같은 딜레이라도 정정 거리(오차)↑. 위치 어긋남은 (딜레이 부족) 또는 (속도 과대) 양쪽 가능.
5. **`LaserOffDelay` 분기 검증(§4)**: 끝점/코너 over-burn이면 `:872` #warning 분기 동작을 로직 분석가와 함께 점검.
6. **로그 활용**: `[Line Weld] Weld Condition ... (LaserOnDelay,LaserOffDelay,JumpDelay,MarkDelay,PolygonDelay,...)`(`:710`)와 `[Line Weld] Jump`/`Mark Start/End Point`(`:877,983`) 로그로 **현장에서 실제 적용된 딜레이값과 시작/끝 좌표를 먼저 확인**한 뒤 튜닝.
---
## 7. 관련 코드 위치
| 내용 | 파일:라인 |
|------|-----------|
| 딜레이 설정 집합 | `SeqUploadWeldingList.cs:632-636` (`SetScannerProcessCondition`) |
| 일반 Line 처리(Jump/Mark) | `SeqUploadWeldingList.cs:861-984` |
| 코너 점프 생략 조건(0.02mm) | `SeqUploadWeldingList.cs:868` |
| LaserOffDelay 미동작 #warning | `SeqUploadWeldingList.cs:872` |
| Ramping 시 코너 처리 | `SeqUploadWeldingList.cs:910-955` |
| 딜레이 us→bit 변환(RTC6) | `ScannerRTC6.cs:691-700` |
| 파라미터 정의/타입 | `LASA/GUI/Scanner/ScannerParam.cs:58-107` |

View File

@ -0,0 +1,230 @@
# 작업지시서 — Laser Interlock (레시피별 Front/Down 방향 발진 차단)
- 작성일: 2026-06-24
- 대상 브랜치: `field/ptop-recipe-scanner-move`
- 작업 분류: 기능 추가 (레시피별 발진 차단 인터락) — 기존 전역 안전 인터락은 **유지**
- ⚠️ 본 문서는 **수정안(검토용)** 입니다. 아직 소스는 변경하지 않았습니다. 작업 PC에서 본 지시서대로 적용하세요.
---
## 1. 목적
레이저 인터락은 **2단계**로 동작하며, 둘 다 **Setting의 `Use Interlock`(HeadThetaInterlockEnabled)가 ON일 때만** 평가한다. (OFF면 인터락 전체 미적용 = 정상 발진)
- **① Use Interlock = 안전 구역 게이트 (기존, 유지)**
- 헤드 각도 θ(D70510)가 Front 또는 Down 각도의 **±Tolerance 안에 있을 때만 발진 허용**, 그 외(어중간/위험 위치)면 **차단**.
- 모터가 위험한 위치에서 발진하는 것을 막는 안전장치. → **기존 로직 그대로 유지.**
- **② Recipe 체크박스 = 레시피별 방향 발진 차단 (신규)**
- 이번에 추가. 모터상 발진해도 되는 안전 구역이라도, **이 레시피에서는 이 방향이면 일부러 차단**.
- 예) Recipe#1에서 DOWN 체크 → Down 각도(예: 180° ±2.5°) 구간은 안전 구역이지만 이 레시피에서는 발진 차단.
### 발진 가부 진리표 (Use ON 기준)
| 상황 | ① Use 안전구역 | ② Recipe 차단 | 결과 |
|---|---|---|---|
| θ가 Front/Down 어느쪽도 아님 | 차단 | – | **발진 X** |
| θ가 Front 구역, Recipe Front 미체크 | 통과 | 통과 | **발진 O** |
| θ가 Front 구역, Recipe Front 체크 | 통과 | 차단 | **발진 X** |
| θ가 Down 구역, Recipe Down 체크 | 통과 | 차단 | **발진 X** |
| (Use OFF) | – | – | **발진 O (인터락 미적용)** |
## 2. 기존 코드 현황 (분석 결과)
### 2-1. Setting (그대로 활용)
`Machines/ILSOS_PrismaticRD_PTOP/Settings/LaserInterlockSetting.cs`
| 프로퍼티 | DisplayName | 기본값 | 본 작업에서의 역할 |
|---|---|---|---|
| `HeadThetaInterlockEnabled` | Use Interlock | false | **마스터 ON/OFF 스위치** — OFF면 ①·② 전체 미동작. ON이면 ① 안전 게이트 + ② 레시피 차단 모두 평가 |
| `HeadThetaFront` | Front Angle (deg) | 0 | Front 방향 기준 각도 |
| `HeadThetaDown` | Down Angle (deg) | 90 | Down 방향 기준 각도 |
| `HeadThetaTolerance` | Tolerance (±deg) | 2.5 | 오차허용값 |
| `HeadThetaPlcScale` | PLC Scale (deg×?) | 10 | `thetaDeg = D70510 / scale` |
→ **각도/Tolerance/Scale/마스터 ON은 Setting에서 읽고**, **방향별 체크(Front/Down on·off)는 레시피 json에서 읽는다.**
### 2-2. 기존 전역 안전 인터락 로직 (→ 유지, 위에 ② 추가)
`Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs:752~782` (`ExecuteWelding` 내부)
- 현재 로직: `HeadThetaInterlockEnabled`가 켜지면 **"Front 또는 Down 부근일 때만 발진 허용, 둘 다 아니면 차단"** = ① 안전 게이트.
- 이 로직은 **그대로 유지**한다. 그 위에 ② 레시피별 방향 차단을 **추가**한다(2-4).
- ⚠️ Manual 경로(`SeqComponentTeachingRun`)에는 ① 안전 게이트조차 **현재 없음** → ①+② 모두 신규 추가(4-E).
### 2-3. 각도 offset 선택은 별개 (건드리지 않음)
`LaserAngleOffsetSelector.Select(...)` (Auto: `SeqAutoRunPTOP.cs:640`, Manual: `SeqComponentTeachingRun.cs:507`) 는
발진 **위치 offset**(Front/Down offset) 선택 용도로, 본 인터락(발진 가부)과 **무관**. 그대로 둔다.
### 2-4. 판정 규칙 (Auto/Manual 공통)
```
// Use Interlock(HeadThetaInterlockEnabled) 가 false 면 인터락 미적용 (정상 발진)
if (!setting.HeadThetaInterlockEnabled) → 통과(발진 O)
// PLC 미연결/미활성 → fail-closed (차단)
if (PLC == null || !PLC.IsActivated) → 차단(알람)
thetaDeg = ReadWord(HEAD_THETA_POSITION) / (scale!=0 ? scale : 1)
nearFront = Math.Abs(thetaDeg - HeadThetaFront) <= HeadThetaTolerance
nearDown = Math.Abs(thetaDeg - HeadThetaDown ) <= HeadThetaTolerance
// ① 안전 구역 게이트 (기존): 안전 구역(Front/Down 부근) 아니면 차단
if (!nearFront && !nearDown) → 차단(알람)
// ② 레시피별 방향 차단 (신규): 체크된 방향의 구역이면 차단
// 레시피 체크값 로드 (Recipe_Laser_Interlock.json): frontChecked=(Front==1), downChecked=(Down==1)
if ((frontChecked && nearFront) || (downChecked && nearDown)) → 차단(알람)
// 위 어디에도 안 걸리면 발진 O
```
- 차단 시: `AlarmManager.Alarm(ALARM_LASER_HEAD_THETA_INTERLOCK)` + `return null`(발진 차단 & 시퀀스 중단).
## 3. 데이터 저장 구조 (신규)
### 3-1. 파일 위치
- 레시피 폴더마다 1개: `_config_recipe/<레시피>/Recipe_Laser_Interlock.json`
- 경로 산출은 기존 `FieldHelper.json` 과 **동일 패턴** 사용 — 활성 레시피 파일 경로의 파일명만 치환.
- 참조: `WeldingComponents/LASA/GUI/Page/MainPage.cs:431,538`
```csharp
string newpath = fullpath.Replace(Path.GetFileName(fullpath), "Recipe_Laser_Interlock.json");
```
- `fullpath` = 활성 레시피 파일 경로(예: `DB.Dao.SystemRecipe.WorkRecipeFile`).
- ⚠️ **적용 시 확인**: `Recipe_Laser_Interlock.json` 이 `FieldHelper.json` 과 **같은 레시피 폴더**에 떨어지는지 1회 검증(같은 `fullpath` 소스를 쓰면 보장됨).
### 3-2. 파일 포맷
```json
{
"Laser Interlock": {
"Front": 0,
"Down": 1
}
}
```
- `0` = 미체크(해당 방향 인터락 해제), `1` = 체크(해당 방향에서 발진 차단).
- 파일이 없으면 기본값 `{ Front:0, Down:0 }` (= 차단 없음)으로 간주하고, 최초 저장 시 생성.
## 4. 작업 범위 및 변경 파일
### 4-A. (신규) 레시피 인터락 모델 + 입출력 헬퍼
신규 파일 1개. 위치: `WeldingComponents/WeldingCommon/Settings/` 또는 `.../Recipes/` (프로젝트 컨벤션에 맞춰).
```csharp
// RecipeLaserInterlock.cs (가칭)
public class RecipeLaserInterlock
{
public bool Front { get; set; } // json 1=true / 0=false
public bool Down { get; set; }
// "Laser Interlock" 래퍼 키로 직렬화: { "Laser Interlock": { "Front":0, "Down":1 } }
// → Newtonsoft 사용 시 래퍼 DTO 또는 JObject로 Front/Down 정수(0/1) 매핑.
public static RecipeLaserInterlock Load(string recipeFilePath); // 파일 없으면 기본(false,false)
public static void Save(string recipeFilePath, RecipeLaserInterlock value);
}
```
- 직렬화 시 bool ↔ 0/1 정수 변환 주의(요청 포맷이 정수 0/1).
- 경로 인자는 **레시피 파일 경로**를 받아 내부에서 파일명 치환(3-1).
### 4-B. (UI) `UCScannerObjectTeachingControlBox.xaml`
`<GroupBox Header="Laser Shot">` 내부 그리드에 **Row 추가** 후, "Run Welding/Stop" 버튼(현재 Grid.Row 2) **하단**에 신규 그룹박스 배치.
1. 내부 그리드 `Grid.RowDefinitions` 에 `<RowDefinition Height="auto"/>` **1개 추가**(현재 3행 → 4행).
2. 신규 그룹박스(예: `Grid.Row="3" Grid.ColumnSpan="2"`):
```xml
<GroupBox Header="Laser Interlock" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0 5 0 0" Style="{StaticResource StGroupBox4th}">
<StackPanel>
<CheckBox Margin="6 4" FontSize="12" FontWeight="Bold" Foreground="#FFBBBBBB"
Content="FRONT INTERLOCK" IsChecked="{Binding IsFrontInterlock}"/>
<CheckBox Margin="6 4" FontSize="12" FontWeight="Bold" Foreground="#FFBBBBBB"
Content="DOWN INTERLOCK" IsChecked="{Binding IsDownInterlock}"/>
</StackPanel>
</GroupBox>
```
- Content 명/스타일은 기존 그룹박스 스타일(`StGroupBox4th`)·텍스트 색(`#FFBBBBBB`) 관례에 맞춤.
### 4-C. (UI VM) `UCScannerObjectTeachingControlBoxViewModel.cs`
1. 프로퍼티 2개 추가 — 변경 시 즉시 레시피 json 저장:
```csharp
private bool _IsFrontInterlock;
public bool IsFrontInterlock {
get => _IsFrontInterlock;
set { SetProperty(ref _IsFrontInterlock, value); SaveLaserInterlock(); }
}
private bool _IsDownInterlock;
public bool IsDownInterlock {
get => _IsDownInterlock;
set { SetProperty(ref _IsDownInterlock, value); SaveLaserInterlock(); }
}
private void SaveLaserInterlock()
=> RecipeLaserInterlock.Save(RecipeFilePath(), new RecipeLaserInterlock { Front = _IsFrontInterlock, Down = _IsDownInterlock });
```
2. 로드 시점:
- 생성자/`InitVariable()` 에서 현재 레시피값 로드.
- **레시피 전환 반영**: 기존 `SYS_SETTING_CHANGED` 구독(`OnSettingReloaded`)과 동일하게, 활성 레시피 변경 이벤트에서 다시 `Load` 해 체크 상태 갱신. (레시피 활성 이벤트는 `RecipeManager` 활성 알림 사용 — 4-D의 sequence와 동일 소스)
- 저장 중 stale 덮어쓰기 방지: 로드로 인한 set 시에는 저장 억제 플래그 사용(기존 패턴과 동일하게 가드).
3. `RecipeFilePath()` 는 `FieldHelper.json` 과 동일 소스(활성 레시피 파일 경로) 반환.
### 4-D. (Auto 시퀀스) `SeqAutoRunPTOP.cs`
`ExecuteWelding` 내 **기존 블록 752~782** 를 아래로 **확장**(① 안전 게이트 유지 + ② 레시피 차단 추가):
```csharp
var thetaSetting = Application.Current.AuroraEnv().SettingEntry[SettingIds.LaserInterlock] as LaserInterlockSetting;
if (thetaSetting != null && thetaSetting.HeadThetaInterlockEnabled)
{
bool blocked = false;
if (_PLC == null || !_PLC.IsActivated) {
Log.W("[ExecuteWelding] LaserInterlock - PLC 미연결/미활성 → 발진 차단(fail-closed)");
blocked = true;
} else {
short raw = _PLC.ReadWord(eWordInput.HEAD_THETA_POSITION).data;
double scale = thetaSetting.HeadThetaPlcScale != 0 ? thetaSetting.HeadThetaPlcScale : 1;
double thetaDeg = raw / scale;
bool nearFront = Math.Abs(thetaDeg - thetaSetting.HeadThetaFront) <= thetaSetting.HeadThetaTolerance;
bool nearDown = Math.Abs(thetaDeg - thetaSetting.HeadThetaDown ) <= thetaSetting.HeadThetaTolerance;
// ① 안전 게이트(기존): 안전 구역 아니면 차단
if (!nearFront && !nearDown) {
Log.W($"[ExecuteWelding] ① 안전구역 벗어남(deg={thetaDeg:F1}) → 발진 차단");
blocked = true;
} else {
// ② 레시피별 방향 차단(신규)
var rl = RecipeLaserInterlock.Load(_SystemParameter.WorkRecipeFile); // = 활성 레시피 파일 경로
if ((rl.Front && nearFront) || (rl.Down && nearDown)) {
Log.W($"[ExecuteWelding] ② 레시피 차단 deg={thetaDeg:F1} Front?{rl.Front}/{nearFront} Down?{rl.Down}/{nearDown} → 발진 차단");
blocked = true;
}
}
}
if (blocked) {
try { AlarmManager.Alarm(AlarmCode_PTOP.ALARM_LASER_HEAD_THETA_INTERLOCK); }
catch (Exception ex) { Log.E($"[ExecuteWelding] 알람 표시 실패: {ex.Message}"); }
return null;
}
}
```
- 위치: `_Scanner.SelectCorrectionTable(2)` / Laser Gate ON **직전**(현재 블록과 동일 지점).
### 4-E. (Manual 시퀀스) `SeqComponentTeachingRun.cs`
- 현재 Manual 경로에는 인터락 블록이 **없음**(① 안전 게이트조차 없음). `DoComponentWelding` 의
`_Scanner.ExecuteList(_WeldingScannerListNo);`(`:571`) **직전**에 4-D와 동일하게 **①+② 모두 신규 추가**.
- 설정 취득: `context.GetSetting<LaserInterlockSetting>(SettingIds.LaserInterlock)` (기존 `:507` 패턴).
- PLC 핸들은 `_plc`, 레시피 경로는 `_SystemParameter.WorkRecipeFile`(또는 활성 레시피 파일 경로) 사용.
- 차단 시 `return null;`.
## 5. 적용 시 확인 사항 (반드시 검증)
1. **D70510 스케일**: `HeadThetaPlcScale` 기본 10(=deg×10) 가정. 현장 실값으로 `thetaDeg`가 맞는지 1회 확인.
2. **레시피 폴더 일치**: `Recipe_Laser_Interlock.json` 이 `FieldHelper.json` 과 같은 폴더에 생성되는지 확인.
3. **알람 코드**: `AlarmCode_PTOP.ALARM_LASER_HEAD_THETA_INTERLOCK` 가 알람 테이블에 등록되어 있는지(기존 블록도 try/catch로 미등록 대비) 확인. 별도 신규 알람 코드가 필요하면 추가 여부 협의.
4. **레시피 전환 시 체크 상태 갱신**: 레시피를 바꾸면 UI 체크박스가 해당 레시피의 json 값으로 즉시 갱신되는지(stale 방지) 확인.
5. **Use Interlock OFF 동작**: Setting의 Use가 꺼져 있으면 체크 여부와 무관하게 정상 발진되는지 확인.
## 6. 작업 순서 권장
1. 4-A 모델/헬퍼(`RecipeLaserInterlock`) 작성 — 로드/세이브 단위 검증.
2. 4-B/4-C UI + VM — 체크 저장/로드, 레시피 전환 갱신 확인.
3. 4-D Auto 시퀀스 교체.
4. 4-E Manual 시퀀스 추가.
5. 5장 검증 항목 점검 → 작업일지 기록.
## 7. 미해결/협의 항목
- 신규 알람 코드 분리 필요 여부(현재는 기존 `ALARM_LASER_HEAD_THETA_INTERLOCK` 재사용 전제).
- `Recipe_Laser_Interlock.json` 의 활성 레시피 경로 소스를 VM/시퀀스에서 동일하게 보장하는 정확한 API(작업 PC에서 `FieldHelper.json` 저장에 쓰는 경로와 일치시킬 것).