docs: 인수인계 산출물 일괄 커밋 (260903~260916)
그동안 미추적으로 쌓여 있던 인수인계 문서를 정리해 올린다. 260903 Door 인터락 현장검증 가이드 / 파워캘 3000W 오적용 환산 260907 Arc 출사 파라미터 시각화 / LaserFrequency 단위계통 / NRE 위험구간 점검 / 레시피화면 사용설명서 260915 레시피화면 작업매뉴얼 260916 PTOP 프로그램 사용설명서 / 자동운전 전환 사전점검 현장용 작업 중 임시본(_backup)은 제외했다.
This commit is contained in:
parent
a733829789
commit
a3238cc2d6
293
Doc/인수인계/260903_Door인터락_현장검증_가이드.html
Normal file
293
Doc/인수인계/260903_Door인터락_현장검증_가이드.html
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Door 인터락 현장 검증 가이드 (2026-09-03)</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#1b1d21; --panel:#25282d; --panel2:#2c3037; --line:#3d434c;
|
||||||
|
--txt:#d8dce2; --dim:#9aa2ad; --key:#bbbbbb;
|
||||||
|
--blue:#3f7fd0; --blue2:#4f92e8;
|
||||||
|
--ok:#3fa66b; --warn:#d9a441; --bad:#d15b5b;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;padding:28px;background:var(--bg);color:var(--txt);
|
||||||
|
font-family:"Malgun Gothic","맑은 고딕",Segoe UI,sans-serif;font-size:14px;line-height:1.65}
|
||||||
|
h1{font-size:22px;margin:0 0 4px;color:#fff}
|
||||||
|
h2{font-size:16px;margin:32px 0 12px;padding-bottom:6px;border-bottom:1px solid var(--line);color:#fff}
|
||||||
|
.sub{color:var(--dim);font-size:13px;margin-bottom:22px}
|
||||||
|
.wrap{max-width:1100px;margin:0 auto}
|
||||||
|
|
||||||
|
.banner{background:linear-gradient(90deg,#2a3d55,#25282d);border-left:4px solid var(--blue);
|
||||||
|
padding:14px 18px;border-radius:4px;margin-bottom:8px}
|
||||||
|
.badge{display:inline-block;padding:2px 9px;border-radius:10px;font-size:11px;font-weight:bold;
|
||||||
|
margin-right:6px;vertical-align:1px}
|
||||||
|
.b-blue{background:var(--blue);color:#fff}
|
||||||
|
.b-warn{background:var(--warn);color:#1b1d21}
|
||||||
|
.b-bad{background:var(--bad);color:#fff}
|
||||||
|
.b-ok{background:var(--ok);color:#fff}
|
||||||
|
|
||||||
|
/* ---- 화면 목업 ---- */
|
||||||
|
.mock{background:var(--panel);border:1px solid var(--line);border-radius:5px;padding:14px;margin:14px 0}
|
||||||
|
.gb{border:1px solid var(--line);border-radius:4px;padding:10px 12px 12px;margin:0 0 10px;position:relative}
|
||||||
|
.gb>.hdr{position:absolute;top:-9px;left:12px;background:var(--panel);padding:0 7px;
|
||||||
|
font-size:11px;font-weight:bold;color:var(--blue2);letter-spacing:.3px}
|
||||||
|
.gb.inner{background:var(--panel2)}
|
||||||
|
.gb.inner>.hdr{background:var(--panel2)}
|
||||||
|
.cols{display:flex;gap:12px;align-items:flex-start}
|
||||||
|
.c-l{flex:1.5}.c-r{flex:2}
|
||||||
|
.row{display:flex;align-items:center;gap:8px;margin:5px 0}
|
||||||
|
.k{flex:0 0 108px;text-align:right;color:var(--key);font-size:12px;font-weight:bold}
|
||||||
|
.v{flex:1;background:#15171a;border:1px solid var(--line);border-radius:3px;
|
||||||
|
padding:4px 8px;font-size:12px;min-height:26px;display:flex;align-items:center}
|
||||||
|
.v.set{border-color:var(--blue);color:#fff;font-weight:bold}
|
||||||
|
.v.ro{background:#1f2226;color:var(--dim)}
|
||||||
|
.u{flex:0 0 44px;color:var(--dim);font-size:11px}
|
||||||
|
|
||||||
|
.tgl{display:flex;border:1px solid var(--line);border-radius:14px;overflow:hidden;margin:7px 0;font-size:11px}
|
||||||
|
.tgl div{flex:1;text-align:center;padding:5px 4px;color:var(--dim)}
|
||||||
|
.tgl div.on{background:var(--blue);color:#fff;font-weight:bold}
|
||||||
|
.tgl div.pick{background:var(--warn);color:#1b1d21;font-weight:bold}
|
||||||
|
|
||||||
|
.btns{display:flex;gap:8px;margin-top:10px}
|
||||||
|
.btn{flex:1;text-align:center;padding:9px 6px;border-radius:3px;background:var(--blue);
|
||||||
|
color:#fff;font-weight:bold;font-size:12px;border:1px solid #35659f}
|
||||||
|
.btn.off{background:#3a3f47;border-color:var(--line)}
|
||||||
|
|
||||||
|
.n{display:inline-flex;align-items:center;justify-content:center;width:19px;height:19px;
|
||||||
|
border-radius:50%;background:var(--warn);color:#1b1d21;font-size:11px;font-weight:bold;
|
||||||
|
margin-right:6px;flex:0 0 19px}
|
||||||
|
.n.red{background:var(--bad);color:#fff}
|
||||||
|
|
||||||
|
table{width:100%;border-collapse:collapse;margin:10px 0;font-size:13px}
|
||||||
|
th,td{border:1px solid var(--line);padding:8px 10px;text-align:left;vertical-align:top}
|
||||||
|
th{background:var(--panel2);color:#fff;font-size:12px}
|
||||||
|
td.c{text-align:center;white-space:nowrap}
|
||||||
|
|
||||||
|
pre{background:#15171a;border:1px solid var(--line);border-left:3px solid var(--blue);
|
||||||
|
border-radius:3px;padding:10px 12px;font-size:12px;overflow-x:auto;
|
||||||
|
font-family:Consolas,monospace;color:#c9d1d9;margin:8px 0}
|
||||||
|
pre.bad{border-left-color:var(--bad)}
|
||||||
|
pre.ok{border-left-color:var(--ok)}
|
||||||
|
code{background:#15171a;padding:1px 5px;border-radius:3px;font-size:12px;
|
||||||
|
font-family:Consolas,monospace;color:#e6b673}
|
||||||
|
|
||||||
|
.note{border-left:3px solid var(--warn);background:#2e2a20;padding:10px 14px;margin:12px 0;border-radius:0 3px 3px 0}
|
||||||
|
.danger{border-left:3px solid var(--bad);background:#2f2222;padding:10px 14px;margin:12px 0;border-radius:0 3px 3px 0}
|
||||||
|
.good{border-left:3px solid var(--ok);background:#1f2c25;padding:10px 14px;margin:12px 0;border-radius:0 3px 3px 0}
|
||||||
|
ul{margin:6px 0;padding-left:20px}
|
||||||
|
li{margin:3px 0}
|
||||||
|
.foot{margin-top:34px;padding-top:12px;border-top:1px solid var(--line);color:var(--dim);font-size:12px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<div class="banner">
|
||||||
|
<h1>메뉴얼 Door 인터락 현장 검증 가이드</h1>
|
||||||
|
<div style="color:var(--dim);font-size:13px;margin-top:4px">
|
||||||
|
커밋 <code>7c97462</code> · ISSUE-260902-01 · 검증일 2026-09-03
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sub">
|
||||||
|
<span class="badge b-blue">대상 화면</span> Manual → Scanner → <b>Manual Laser Power Control</b>
|
||||||
|
· 소스 <code>UCScannerLaserControl.xaml</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="danger">
|
||||||
|
<b>시작 전 필수</b>
|
||||||
|
<ul>
|
||||||
|
<li>레이저 파워미터(측정기)는 <b>치우고 덤프 타겟</b>을 놓을 것 — 어제 누수 건도 있고 테스트 중 재파손 위험</li>
|
||||||
|
<li>출력은 <b>10%</b> 로 낮춰서 진행. 정상 파워측정 조건으로 하지 말 것</li>
|
||||||
|
<li>보안경 착용, 문 여는 사람과 화면 보는 사람 <b>2인</b> 권장</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>1. 화면 배치와 조작 위치</h2>
|
||||||
|
<p style="color:var(--dim);font-size:13px;margin:0 0 4px">
|
||||||
|
아래는 XAML 기준 실제 배치입니다. <b>Watt 패널은 <code>Visibility="Collapsed"</code> 라 화면에 안 나옵니다</b> — % 패널로만 진행합니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mock">
|
||||||
|
<div class="gb">
|
||||||
|
<div class="hdr">Manual Laser Power Control</div>
|
||||||
|
<div class="cols">
|
||||||
|
|
||||||
|
<div class="c-l">
|
||||||
|
<div class="gb inner">
|
||||||
|
<div class="hdr">Control Setting</div>
|
||||||
|
<div class="row"><span class="k"><span class="n">2</span>Power #1</span><span class="v set">10</span><span class="u">%</span></div>
|
||||||
|
<div class="row"><span class="k">Power #2</span><span class="v set">0</span><span class="u">%</span></div>
|
||||||
|
<div class="row"><span class="k">Frequency</span><span class="v">1</span><span class="u">kHz</span></div>
|
||||||
|
<div class="row"><span class="k">Pulse Width</span><span class="v">1000000</span><span class="u">us</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="c-r">
|
||||||
|
<div class="gb inner">
|
||||||
|
<div class="hdr">Laser Shot Control</div>
|
||||||
|
<div class="cols">
|
||||||
|
<div style="flex:1">
|
||||||
|
<div class="gb" style="background:#212429">
|
||||||
|
<div class="hdr" style="background:#212429">Timer Set</div>
|
||||||
|
<div style="font-size:11px;color:var(--dim);margin-bottom:2px"><span class="n">3</span>연속출사로 전환</div>
|
||||||
|
<div class="tgl">
|
||||||
|
<div class="pick">No Use Spot Time</div><div>Use Spot Time</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:10px;color:var(--dim);margin:-2px 0 8px 2px">
|
||||||
|
← 이쪽이 <b>자동 오프 없는 연속출사</b> (이번 사고 경로)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="font-size:11px;color:var(--dim);margin-bottom:2px"><span class="n">4</span>실제 발진</div>
|
||||||
|
<div class="tgl">
|
||||||
|
<div class="pick">Laser Fire</div><div>No Laser (전압만)</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:10px;color:var(--dim);margin:-2px 0 0 2px">
|
||||||
|
※ DA1/DA2 전압 표시는 <b>No Laser 쪽일 때만</b> 보임
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex:1">
|
||||||
|
<div class="gb" style="background:#212429">
|
||||||
|
<div class="hdr" style="background:#212429">Laser Shot Result</div>
|
||||||
|
<div class="row"><span class="k">List</span><span class="v ro">N/A</span></div>
|
||||||
|
<div class="row"><span class="k">Repeat Count</span><span class="v ro">0</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="btns">
|
||||||
|
<div class="btn"><span class="n red">5</span>Laser On</div>
|
||||||
|
<div class="btn off">Laser Off</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note">
|
||||||
|
<b><span class="n">1</span> 이 화면을 반드시 먼저 띄워야 합니다.</b>
|
||||||
|
출사 중 Door 감시 타이머는 <code>VisibleChanged</code> 로 시작됩니다. 화면을 한 번도 안 열면
|
||||||
|
감시가 돌지 않습니다. (단, 이 작업 자체가 이 화면에서만 하는 것이라 실사용상 문제는 없습니다)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>2. 검증 순서</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th style="width:36px">#</th><th style="width:190px">조작</th><th>기대 결과</th><th style="width:150px">판정</th></tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="c"><b>T1</b></td>
|
||||||
|
<td><b>문을 연 채</b> [Laser On]<br><span style="color:var(--dim);font-size:12px">(가장 안전한 첫 테스트)</span></td>
|
||||||
|
<td>출사 <b>안 됨</b> + 노란 경고 토스트<br>
|
||||||
|
<code>Door Open 상태입니다. 문을 닫고 다시 시도하세요.</code></td>
|
||||||
|
<td class="c"><span class="badge b-ok">차단</span> 정상<br><span class="badge b-bad">출사</span> → §3</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="c"><b>T2</b></td>
|
||||||
|
<td>문 닫고 [Laser On]</td>
|
||||||
|
<td>정상 출사 시작. 계속 출사 상태 유지<br>(자동 오프 없음)</td>
|
||||||
|
<td class="c"><span class="badge b-ok">출사</span> 정상<br><span class="badge b-bad">차단</span> → §3</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr style="background:#2a2226">
|
||||||
|
<td class="c"><b>T3</b></td>
|
||||||
|
<td><b>출사 중에 문을 연다</b><br><span style="color:var(--bad);font-size:12px">★ 이번 사고 시나리오</span></td>
|
||||||
|
<td><b>약 0.2초 내 강제 OFF</b> + 빨간 토스트<br>
|
||||||
|
<code>Door Open 감지 → 레이저를 강제 OFF 했습니다.</code></td>
|
||||||
|
<td class="c"><span class="badge b-ok">OFF</span> 정상<br><span class="badge b-bad">유지</span> → §3</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="c"><b>T4</b></td>
|
||||||
|
<td>문을 다시 닫는다<br>(아무것도 누르지 않음)</td>
|
||||||
|
<td><b>자동 재출사 안 됨</b> — 의도된 동작</td>
|
||||||
|
<td class="c"><span class="badge b-ok">정지유지</span></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="c"><b>T5</b></td>
|
||||||
|
<td>[Laser On] 다시 누름</td>
|
||||||
|
<td>정상 출사 재개</td>
|
||||||
|
<td class="c"><span class="badge b-ok">출사</span></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="c"><b>T6</b></td>
|
||||||
|
<td>[Laser Off] 로 종료</td>
|
||||||
|
<td>출사 종료, 감시 래치 해제</td>
|
||||||
|
<td class="c">—</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>3. 로그 확인 — 극성 판별</h2>
|
||||||
|
<p style="margin:0 0 6px">로그에서 <code>[DoorInterlock]</code> 으로 검색하면 전부 나옵니다.</p>
|
||||||
|
|
||||||
|
<div class="good">
|
||||||
|
<b>정상 (문 열림에서 True 가 하나라도 뜸)</b>
|
||||||
|
<pre class="ok">[DoorInterlock][ScannerLaserControl] Door OPEN 감지 → 이후 레이저 출사 차단 (PLC=True, IO_DI=True)
|
||||||
|
[DoorInterlock] 출사 중 Door OPEN 감지 → 레이저 강제 OFF 실행 (직접 출사 중). PLC비트=True, IO접점=True, PLC연결=True, IO연결=True
|
||||||
|
[DoorInterlock] 레이저 강제 OFF 완료. 재출사하려면 문을 닫고 Shot On 을 다시 누르십시오.</pre>
|
||||||
|
<div style="font-size:12px;color:var(--dim)">PLC / IO 둘 중 하나만 True 여도 정상 동작합니다(OR 판정). 어느 쪽이 False 인지는 알려주세요 — 배선/맵 확인 대상입니다.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="danger">
|
||||||
|
<b>이상 ①: 문 열었는데 둘 다 False</b> → T1/T3 에서 차단이 안 됨
|
||||||
|
<pre class="bad">[DoorInterlock] ... (PLC=False, IO_DI=False)</pre>
|
||||||
|
<ul style="font-size:13px">
|
||||||
|
<li><code>PLC연결=False</code> / <code>IO연결=False</code> 면 → 디바이스 미연결. 연결부터 확인</li>
|
||||||
|
<li>둘 다 연결인데 False 면 → <b>접점 극성 반대(b접점)</b> 또는 PLC 비트 미할당</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="danger">
|
||||||
|
<b>이상 ②: 문 닫았는데 True</b> → T2 에서 출사가 막힘 (반대 증상)
|
||||||
|
<pre class="bad">문 닫힘 상태인데 → (PLC=True, IO_DI=True)</pre>
|
||||||
|
<div style="font-size:13px">극성 반대 확정입니다. 판정부 한 줄만 뒤집으면 되니 로그값 그대로 보내주세요.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note">
|
||||||
|
<b>롱텀 추적용 하트비트</b> — 출사 중에는 10초마다 아래가 남습니다. 1시간 출사 구간 로그 공백이 없어집니다.
|
||||||
|
<pre>[DoorInterlock] 직접 출사 중... Door=CLOSE, DA1=1.000V, DA2=0.000V, PLC비트=False, IO접점=False, ...</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>4. 이 인터락이 <u>못 막는</u> 구간</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th style="width:210px">경로</th><th>차단 시점</th><th style="width:110px">완전성</th></tr>
|
||||||
|
<tr>
|
||||||
|
<td>파워측정 직접 출사<br><span style="color:var(--dim);font-size:12px">(No Use Spot Time)</span></td>
|
||||||
|
<td>출사 시작 전 + <b>출사 중 100ms 주기</b></td>
|
||||||
|
<td class="c"><span class="badge b-ok">즉시</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Teaching Run 시퀀스</td>
|
||||||
|
<td>시퀀스 진입 시 + <b>객체 사이사이</b><br>
|
||||||
|
<span style="color:var(--dim);font-size:12px">리스트 1개 실행 중에는 개입 불가</span></td>
|
||||||
|
<td class="c"><span class="badge b-warn">부분</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Auto 운전</td>
|
||||||
|
<td><b>이번 범위 아님</b> — <code>SeqAutoRunPTOP</code> 은 자체 <code>ExecuteWelding</code> 사용</td>
|
||||||
|
<td class="c"><span class="badge b-bad">미적용</span></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="danger">
|
||||||
|
<b>본질적 한계 — 반드시 같이 보고할 것</b><br>
|
||||||
|
이건 <b>SW 방어이지 안전 인터락이 아닙니다.</b> Door 감시가 UI 스레드 DispatcherTimer 기반이라
|
||||||
|
UI가 멈추면 감시도 같이 멈춥니다. Class 4 레이저 기준 최종 차단은
|
||||||
|
<b>PLC / 하드와이어 안전회로</b>로 별도 협의가 필요합니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="foot">
|
||||||
|
변경 파일 3개 — <code>AlarmManager.cs</code> (PLC비트 OR IO접점 판정) ·
|
||||||
|
<code>UCScannerLaserControlViewModel.cs</code> (시작 게이트 + 출사 중 감시) ·
|
||||||
|
<code>SeqComponentTeachingRun.cs</code> (주석 처리됐던 차단 복구 + 출사 직전 게이트)<br>
|
||||||
|
미결: EMO 차단 복구 · Auto 시작 게이트(<code>UCMainAutoRunViewModel</code> 399~431행 블록 주석) · 하드와이어 안전회로
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
281
Doc/인수인계/260903_파워캘_3000W오적용_레시피환산.md
Normal file
281
Doc/인수인계/260903_파워캘_3000W오적용_레시피환산.md
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
# 파워캘 CSV 3000W 오적용 및 레시피 Power 환산 (2026-09-03)
|
||||||
|
|
||||||
|
> **목적** : 2000W 발진기에 3000W 기준 파워캘 CSV가 적용되어 있어 레시피 지령값의 2/3만
|
||||||
|
> 실제 출사되던 건의 원인·근거·환산값을 남긴다.
|
||||||
|
>
|
||||||
|
> - 대상 설비 : PtoP 각형화 단동 용접기 (유니셀)
|
||||||
|
> - 관련 문서 : `Doc/인수인계/2026_08_28_레이저발진기교체_및_진행현황.md` §2.4
|
||||||
|
> - 파라미터 원본 : `docs/파라미터PTOP_260903.xlsx` 시트 `260903`
|
||||||
|
|
||||||
|
**표기 규칙** (기존 인수인계 문서와 동일)
|
||||||
|
|
||||||
|
| 표기 | 의미 |
|
||||||
|
|---|---|
|
||||||
|
| 🟩 **소스확인** | PC 소스/설정 파일에서 직접 대조해 확인한 사실 |
|
||||||
|
| 🟦 **현장기록** | 현장 로그·시험 기록에 남아 있는 값 |
|
||||||
|
| 🟨 **미확보** | 아직 확인되지 않음. 이어받는 사람이 채워야 할 항목 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 한 줄 요약
|
||||||
|
|
||||||
|
발진기는 **2000W/2000W (Core/Ring, 싱글모드)** 인데 파워캘 CSV가 **만점 3000W** 짜리로 물려 있어,
|
||||||
|
레시피에 적힌 W의 **2/3만 실제로 출사**되고 있었다. CSV를 2000W 기준으로 재작성했으므로
|
||||||
|
**레시피의 Power #1 / #2 를 × 2/3 으로 환산**해야 종전과 같은 실출력이 유지된다.
|
||||||
|
|
||||||
|
```
|
||||||
|
레시피 1500W → 지령 50% → DA 5.0V → 2000W × 50% = 1000W 출사
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 원인 🟩
|
||||||
|
|
||||||
|
### 2.1 레시피 W 는 발진기로 그대로 가지 않는다
|
||||||
|
|
||||||
|
`SeqUploadWeldingList.cs:639-671` — 자동 용접 시 파워를 싣는 경로.
|
||||||
|
|
||||||
|
```
|
||||||
|
레시피 LaserPower1/2 (W)
|
||||||
|
→ CalibrateWattToPercent(W) ← 파워캘 CSV 조회
|
||||||
|
→ percentLaser1/2 (0~100)
|
||||||
|
→ WriteDA(DA1/DA2, percent) ← ×40.95 → 12bit DAC → 0~10V
|
||||||
|
→ 발진기 아날로그 입력 = 자기 정격의 그 %
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSV의 만점이 곧 "설비가 믿고 있는 발진기 정격"** 이다.
|
||||||
|
`MaxCenterPower` / `MaxRingPower` 도 레이저 Config 가 아니라 **CSV 마지막 행**에서 읽는다
|
||||||
|
(`LaserPowerCalParam.cs:70-71`). 발진기를 바꿔도 CSV 를 안 바꾸면 소프트웨어는 옛 정격으로 계산한다.
|
||||||
|
|
||||||
|
### 2.2 실제로 참조되는 열
|
||||||
|
|
||||||
|
현장은 로그상 `Normal Models::` 분기(`TruFiberType` 미지정)를 탄다. 이 분기의 `CalibrateWattToPercent` 가
|
||||||
|
보는 열은 두 개뿐이다.
|
||||||
|
|
||||||
|
| 열 | 이름 | 용도 |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | Index (%) | **반환값 — 그대로 `WriteDA` 지령** |
|
||||||
|
| 1 | DesiredPower | 이 분기에선 미사용 |
|
||||||
|
| 2 | CalibratedPower | **조회 키 — 레시피 W 를 여기서 찾음** |
|
||||||
|
| 3 | volt | 이 분기에선 미사용 |
|
||||||
|
|
||||||
|
> `TruFiberType` 을 `TruFiberP` / `CalPercentToMeasuredVoltage` 로 바꾸면 3열(전압)을 쓰는
|
||||||
|
> 다른 분기로 넘어간다. CSV 를 새로 만들 때는 **어느 분기를 타는지부터 확인**할 것.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 근거
|
||||||
|
|
||||||
|
### 3.1 로그 실측 🟦
|
||||||
|
|
||||||
|
`Original Power → Calibrated Percent` 가 전부 정확히 `W ÷ 3000 × 100` 이다.
|
||||||
|
|
||||||
|
| 로그 | 지령 W | Calibrated % | 검산 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Aurora_20260825.log | 150 | 5.000 | 150/3000 |
|
||||||
|
| Aurora_20260826.log | 500 | 16.667 | 500/3000 |
|
||||||
|
| Aurora_20260826.log | 800 | 26.667 | 800/3000 |
|
||||||
|
| Aurora_20260826.log | 600 | 20.000 | 600/3000 |
|
||||||
|
| Aurora_20260826.log | 1100 | 36.667 | 1100/3000 |
|
||||||
|
|
||||||
|
Center · Ring 양쪽 모두 3000 으로 나뉘었다.
|
||||||
|
|
||||||
|
### 3.2 CSV 대조 🟩
|
||||||
|
|
||||||
|
| 파일 | 관계 | 성격 |
|
||||||
|
|---|---|---|
|
||||||
|
| `docs/3000W_CenterPower.csv` · `3000W_RingPower.csv` (구) | 파워 = **30 × Index** | 선형, 보정 없음 (CalibratedPower = DesiredPower) |
|
||||||
|
| `docs/2000W_CenterPower.csv` · `2000W_RingPower.csv` (신) | 파워 = **20 × Index** | 선형, 보정 없음 |
|
||||||
|
|
||||||
|
**양쪽 다 선형이므로 환산계수 `2000/3000 = 2/3` 는 근사가 아니라 정확값이다.**
|
||||||
|
|
||||||
|
`volt` 열이 Center/Ring 미세하게 다르지만(50% 에서 5.0 vs 4.8) `Normal Models` 분기는 이 열을 쓰지 않는다.
|
||||||
|
|
||||||
|
### 3.3 ⭐ 교차검증 — 「출력을 올려야 했다」의 정체 🟦
|
||||||
|
|
||||||
|
기존 인수인계 문서 §2.4 에 **발진기 교체 후 기존 출력으로는 접합이 되지 않아 출력을 올렸다**는 기록이 있다.
|
||||||
|
이번 건으로 계산해 보면 **실제 출사 파워는 거의 그대로였다.**
|
||||||
|
|
||||||
|
| 12_1page (CC Anode) | 레시피 값 | 당시 CSV | 실제 출사 | 결과 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 교체 전 (3000W 발진기) | 450 | 3000 (정상) | **450W** | 접합 O |
|
||||||
|
| 교체 직후 | 450 | 3000 (오적용) | **300W** | 접합 X |
|
||||||
|
| 탐색 | 500 / 550 | 3000 (오적용) | 333 / 367W | 접합 X |
|
||||||
|
| 최종 적용 | **650** | 3000 (오적용) | **433W** | 접합 O |
|
||||||
|
|
||||||
|
즉 **접합에 필요한 실출력은 계속 430~450W 였고**, 레시피 숫자를 450 → 650 으로 올린 것은
|
||||||
|
공정상 필요가 아니라 **CSV 오적용을 사람이 손으로 보정한 것**이었다.
|
||||||
|
|
||||||
|
× 2/3 환산으로 650 → **433** 이 되는데, 이는 교체 전 정상값 450 과 사실상 같은 실출력이다.
|
||||||
|
(싱글모드 전환으로 파워밀도가 높아져 약간 낮은 값에서 접합되는 것도 방향이 맞다.)
|
||||||
|
|
||||||
|
> 12_2page · 12_3page 는 Ramping 변경(50%→70%)과 싱글모드 효과가 섞여 있어 이만큼 깔끔하게 떨어지지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 환산
|
||||||
|
|
||||||
|
### 4.1 규칙
|
||||||
|
|
||||||
|
```
|
||||||
|
새 레시피 W = 기존 레시피 W × 2/3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 환산 대상 / 제외 🟩
|
||||||
|
|
||||||
|
| 항목 | 환산 | 이유 |
|
||||||
|
|---|---|---|
|
||||||
|
| **Power #1 (Core, W)** | **× 2/3** | 절대 W → 지령 % 로 변환되는 값 |
|
||||||
|
| **Power #2 (Ring, W)** | **× 2/3** | Ring 표도 만점 3000 선형이었음 |
|
||||||
|
| Ramping Start/End Power | 그대로 | **%** 단위. `percentLaser1 × RampingStartPower / 100` 이라 자동 추종 (`SeqUploadWeldingList.cs:1319-1323`) |
|
||||||
|
| Laser Duty | 그대로 | % |
|
||||||
|
| Draw/Jump speed, 각종 Delay | 그대로 | 파워 무관 |
|
||||||
|
| Frequency / Pulse Duration | 그대로 | 파워 무관 |
|
||||||
|
| Wobbel · Zigzag | 그대로 | 파워 무관 |
|
||||||
|
| Center/Ring 비율 | 자동 | 코드가 `round(100×P1/(P1+P2))` 로 매번 계산 |
|
||||||
|
| 레이저 프로그램 워드 IW03/IW04 | 자동 | Power #1/#2 에서 파생 (`SeqWaitWeldOrChangePage.cs:205-223`) |
|
||||||
|
|
||||||
|
### 4.3 환산표 — `파라미터PTOP_260903.xlsx` 시트 `260903` 기준 🟦
|
||||||
|
|
||||||
|
| 레시피 | No | Page | 항목 | 기존 (W) | 지령 % | **변경 (W)** |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| cc welding | 12 | 1 (직렬 음) | Power #1 | 650 | 21.67 | **433** |
|
||||||
|
| | | | Power #2 | 1050 | 35.00 | **700** |
|
||||||
|
| | | 2 (sensing) | Power #1 | 550 | 18.33 | **367** |
|
||||||
|
| | | | Power #2 | 1050 | 35.00 | **700** |
|
||||||
|
| | | 3 (직렬 양) | Power #1 | 600 | 20.00 | **400** |
|
||||||
|
| | | | Power #2 | 1200 | 40.00 | **800** |
|
||||||
|
| U-Frame_pre | 21 | 1~6 | Power #1 | 700 | 23.33 | **467** |
|
||||||
|
| | | | Power #2 | 900 | 30.00 | **600** |
|
||||||
|
| U-Frame_Back_pre | 22 | 1~6 | Power #1 | 700 | 23.33 | **467** |
|
||||||
|
| | | | Power #2 | 900 | 30.00 | **600** |
|
||||||
|
| U-Frame_Main | 23 | 1~6 | Power #1 | 700 | 23.33 | **467** |
|
||||||
|
| | | | Power #2 | 900 | 30.00 | **600** |
|
||||||
|
| U-Frame_Back_Main | 24 | 1~6 | Power #1 | 700 | 23.33 | **467** |
|
||||||
|
| | | | Power #2 | 900 | 30.00 | **600** |
|
||||||
|
| Top_Frame | 31 | 1~2 | Power #1 | 700 | 23.33 | **467** |
|
||||||
|
| | | | Power #2 | 900 | 30.00 | **600** |
|
||||||
|
| Top_Frame | 32 | 1~2 | Power #1 | 700 | 23.33 | **467** |
|
||||||
|
| | | | Power #2 | 900 | 30.00 | **600** |
|
||||||
|
|
||||||
|
고유값 7개 — `550→367, 600→400, 650→433, 700→467, 900→600, 1050→700, 1200→800`
|
||||||
|
|
||||||
|
### 4.4 검산
|
||||||
|
|
||||||
|
**지령 % 불변** — 환산 목적이 "기존에 실제로 나가던 파워 재현"이므로 지령 % 와 DA 전압은 변하지 않아야 한다.
|
||||||
|
|
||||||
|
```
|
||||||
|
기존 : 700W ÷ 30 = 23.33% → DA 2.33V → 2000W × 23.33% = 467W
|
||||||
|
신규 : 467W ÷ 20 = 23.35% → DA 2.33V → 2000W × 23.35% = 467W ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
**Center/Ring 비율 유지** — 반올림 후에도 전부 동일하다.
|
||||||
|
|
||||||
|
| | 기존 | 신규 |
|
||||||
|
|---|---|---|
|
||||||
|
| cc 1page (650/1050) | 38 | 38 (433/700) |
|
||||||
|
| cc 2page (550/1050) | 34 | 34 (367/700) |
|
||||||
|
| cc 3page (600/1200) | 33 | 33 (400/800) |
|
||||||
|
| U-Frame · Top (700/900) | 44 | 44 (467/600) |
|
||||||
|
|
||||||
|
정수 반올림 오차는 최대 0.08 % (433 vs 433.33) 로 무시 가능하다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. ⚠ 주의
|
||||||
|
|
||||||
|
### 5.1 환산 전에 신 CSV 만 적용된 상태는 위험하다 🟩
|
||||||
|
|
||||||
|
| 기존 레시피 값 | 신 CSV 에서의 결과 |
|
||||||
|
|---|---|
|
||||||
|
| 2000 이하 | 의도의 **1.5배 과출력** |
|
||||||
|
| 2000 초과 | `CalibrateWattToPercent` 가 구간을 못 찾아 **0 반환 → 0V, 무출력** |
|
||||||
|
|
||||||
|
같은 레시피 안에서 과출력 라인과 무출력 라인이 섞인다. 환산은 한 번에 끝낼 것.
|
||||||
|
(환산 후에는 모든 값이 2000 이하가 되어 이 함정은 사라진다.)
|
||||||
|
|
||||||
|
### 5.2 지령 상한 클램프가 없다 🟩
|
||||||
|
|
||||||
|
`percentLaser1/2` 는 `WriteDA` 로 가기 전 **상한 검사를 받지 않는다** (`SeqUploadWeldingList.cs:665-671`).
|
||||||
|
램핑 구간만 `<= 100 ? … : 100` 으로 막혀 있다.
|
||||||
|
|
||||||
|
따라서 **열 구성이 어긋난 CSV**(예: 0열에 Index % 대신 W 가 들어간 파일)를 물리면
|
||||||
|
`WriteDA(600)` → `600 × 40.95 = 24570` → DAC 풀스케일 부근 → **정격 전출력**이 나간다.
|
||||||
|
CSV 를 교체할 때는 0열이 0~100 인지 반드시 확인할 것.
|
||||||
|
|
||||||
|
### 5.3 환산은 W 를 복원할 뿐, 용접 결과를 복원하지 않는다 🟦
|
||||||
|
|
||||||
|
듀얼모드(3000W/3000W) → **싱글모드(2000W/2000W)** 전환이라 빔 품질(BPP/M²)이 달라졌다.
|
||||||
|
같은 W 라도 집광 스팟이 작아 **파워밀도(W/mm²)가 훨씬 높다.**
|
||||||
|
|
||||||
|
| | 환산으로 해결 | 남는 것 |
|
||||||
|
|---|---|---|
|
||||||
|
| 실제 출사 W | ✅ 종전과 동일 복원 | — |
|
||||||
|
| 용입 깊이 | ❌ | 싱글모드라 더 깊음 |
|
||||||
|
| 비드 폭 | ❌ | 싱글모드라 더 좁음 |
|
||||||
|
|
||||||
|
현장 기록에도 싱글모드는 **"깊게 쏨 / 얇게 나옴"** 으로 적혀 있다.
|
||||||
|
환산값은 **재튜닝의 출발점**이지 최종값이 아니다. 실물 비드 기준 재검증이 필요하다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 추적성 — 어떤 CSV 가 물렸는지 확인하는 방법 🟩
|
||||||
|
|
||||||
|
**소프트웨어는 적용된 CSV 의 경로도 값도 로그에 남기지 않는다.** 남는 것은 아래뿐이다.
|
||||||
|
|
||||||
|
| 방법 | 얻는 것 |
|
||||||
|
|---|---|
|
||||||
|
| `_config_setting/LaserPowerCalParam.json` | **현재** 물린 Center/Ring CSV 경로 (이력 없음, 덮어쓰기) |
|
||||||
|
| 로그 `AURORA_SYS_SETTING_CHANGED :: Param[LaserPowerCalParam]` | 설정을 **바꾼 시점** (경로는 안 나옴) |
|
||||||
|
| 로그 `Original Power1 : X … Calibrated Percent1 : Y` | **X ÷ Y × 100 = 그 시점 CSV 만점** ← 가장 확실한 역산 수단 |
|
||||||
|
| CSV 파일 수정일시 | 경로 그대로 두고 내용만 고친 경우의 유일한 단서 |
|
||||||
|
|
||||||
|
> 출사 시마다 `Initialize()` 가 CSV 를 다시 읽으므로(`UCScannerLaserControlViewModel.cs:332`, `:422`,
|
||||||
|
> `SeqUploadWeldingList.cs:154`), 앱 재기동 없이 파일 내용만 바꿔도 즉시 반영된다.
|
||||||
|
|
||||||
|
### 이력 확인 결과 🟦
|
||||||
|
|
||||||
|
| 로그 | `Param[LaserPowerCalParam]` 변경 |
|
||||||
|
|---|---|
|
||||||
|
| Aurora_20260825.log | 0 건 |
|
||||||
|
| Aurora_20260826.log | 0 건 |
|
||||||
|
| Aurora_20260902.log | **1 건 — 11:44:32 `From[USER_via_UI]`** |
|
||||||
|
|
||||||
|
2026-09-02 11:44:32 이 CSV 경로 교체 시점이고, 이어진 13:45~15:03 의 `Laser On` 58 회가
|
||||||
|
2000W 재캘리브레이션 작업이다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 이어받는 사람에게
|
||||||
|
|
||||||
|
- 🟨 **발진기 교체 시에는 CSV 재작성과 레시피 W 환산을 반드시 한 세트로 처리할 것.**
|
||||||
|
둘 중 하나만 하면 이번과 같이 조용히 1.5배 또는 2/3 로 어긋난다.
|
||||||
|
- 🟨 파워 이상 신고가 오면 §6 의 `Original Power1 / Calibrated Percent1` 비로 CSV 만점부터 역산할 것.
|
||||||
|
- 🟨 §5.3 의 싱글모드 재검증은 **미완**이다. 환산 적용 후 비드 단면 확인 필요.
|
||||||
|
- 🟨 CSV 경로·값을 로그에 남기는 계측을 추가하면 이런 추적이 훨씬 쉬워진다.
|
||||||
|
출사 시작 로그(`UCScannerLaserControlViewModel.cs:361`, `:452`)에 CSV 경로 + Max 파워 +
|
||||||
|
산출된 `powerVolt1/2` 를 붙이는 것만으로 충분하다. **미적용.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 참고 파일 위치
|
||||||
|
|
||||||
|
| 구분 | 경로 |
|
||||||
|
|---|---|
|
||||||
|
| 파라미터 원본 | `docs/파라미터PTOP_260903.xlsx` 시트 `260903`, 환산 이력 시트 `260903_2000W환산` |
|
||||||
|
| 구 파워캘 CSV | `docs/3000W_CenterPower.csv` · `docs/3000W_RingPower.csv` |
|
||||||
|
| 신 파워캘 CSV | `docs/2000W_CenterPower.csv` · `docs/2000W_RingPower.csv` |
|
||||||
|
| 현장 로그 | `docs/Aurora_20260825.log` · `20260826` · `20260902` |
|
||||||
|
| 파워캘 설정 클래스 | `WeldingComponents/WeldingCommon/Settings/LaserPowerCalParam.cs` |
|
||||||
|
| 자동 용접 파워 적용부 | `WeldingComponents/WeldingCommon/Sequences/Main/SeqUploadWeldingList.cs:639-671` |
|
||||||
|
| 수동 출사 파워 적용부 | `WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControlViewModel.cs:329-345`, `:419-436` |
|
||||||
|
| DA 전압 변환 | `AuroraDevices/AuroraLaserScanner/ScannerRTC5.cs:342-351` |
|
||||||
|
| 발진기 교체 경위 | `Doc/인수인계/2026_08_28_레이저발진기교체_및_진행현황.md` §2.4 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 변경 이력
|
||||||
|
|
||||||
|
| 일자 | 작성 | 내용 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-09-03 | 노도연 | 최초 작성. 3000W CSV 오적용 원인·근거·환산표·주의사항 정리. 기존 인수인계 §2.4 「출력을 올려야 했다」가 CSV 오적용 보정이었음을 교차검증 |
|
||||||
BIN
Doc/인수인계/260907_Arc용접_설정가이드_현장용.pptx
Normal file
BIN
Doc/인수인계/260907_Arc용접_설정가이드_현장용.pptx
Normal file
Binary file not shown.
661
Doc/인수인계/260907_Arc출사_파라미터_시각화.html
Normal file
661
Doc/인수인계/260907_Arc출사_파라미터_시각화.html
Normal file
@ -0,0 +1,661 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Arc 출사 파라미터 시각화 — PTOP (260907)</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
color-scheme: light;
|
||||||
|
--plane:#f9f9f7; --surface-1:#fcfcfb;
|
||||||
|
--text-primary:#0b0b0b; --text-secondary:#52514e; --muted:#898781;
|
||||||
|
--grid:#e1e0d9; --axis:#c3c2b7; --border:rgba(11,11,11,0.10);
|
||||||
|
--series-1:#2a78d6; /* 마크(용접) 경로 */
|
||||||
|
--series-2:#eb6834; /* 점프 경로 */
|
||||||
|
--series-3:#1baf7a; /* 와블 오버레이 */
|
||||||
|
--st-good:#0ca30c; --st-warn:#fab219; --st-serious:#ec835a; --st-crit:#d03b3b;
|
||||||
|
/* blue sequential ramp = 파워 크기 */
|
||||||
|
--p250:#86b6ef; --p350:#5598e7; --p450:#2a78d6; --p550:#1c5cab; --p650:#104281;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){
|
||||||
|
:root:where(:not([data-theme="light"])){
|
||||||
|
color-scheme: dark;
|
||||||
|
--plane:#0d0d0d; --surface-1:#1a1a19;
|
||||||
|
--text-primary:#ffffff; --text-secondary:#c3c2b7; --muted:#898781;
|
||||||
|
--grid:#2c2c2a; --axis:#383835; --border:rgba(255,255,255,0.10);
|
||||||
|
--series-1:#3987e5; --series-2:#d95926; --series-3:#199e70;
|
||||||
|
--p250:#184f95; --p350:#256abf; --p450:#3987e5; --p550:#6da7ec; --p650:#9ec5f4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:root[data-theme="dark"]{
|
||||||
|
color-scheme: dark;
|
||||||
|
--plane:#0d0d0d; --surface-1:#1a1a19;
|
||||||
|
--text-primary:#ffffff; --text-secondary:#c3c2b7; --muted:#898781;
|
||||||
|
--grid:#2c2c2a; --axis:#383835; --border:rgba(255,255,255,0.10);
|
||||||
|
--series-1:#3987e5; --series-2:#d95926; --series-3:#199e70;
|
||||||
|
--p250:#184f95; --p350:#256abf; --p450:#3987e5; --p550:#6da7ec; --p650:#9ec5f4;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{
|
||||||
|
margin:0; background:var(--plane); color:var(--text-primary);
|
||||||
|
font-family:system-ui,-apple-system,"Segoe UI","Malgun Gothic",sans-serif;
|
||||||
|
font-size:14px; line-height:1.6;
|
||||||
|
}
|
||||||
|
.wrap{max-width:1280px; margin:0 auto; padding:0 24px 80px}
|
||||||
|
header.banner{
|
||||||
|
background:linear-gradient(100deg,#104281,#2a78d6);
|
||||||
|
color:#fff; padding:28px 0 24px; margin-bottom:28px;
|
||||||
|
}
|
||||||
|
header.banner .wrap{padding-bottom:0}
|
||||||
|
header.banner h1{margin:0 0 6px; font-size:23px; letter-spacing:-0.01em}
|
||||||
|
header.banner p{margin:0; opacity:.88; font-size:13px}
|
||||||
|
.badges{margin-top:14px; display:flex; gap:8px; flex-wrap:wrap}
|
||||||
|
.badge{
|
||||||
|
background:rgba(255,255,255,.16); border:1px solid rgba(255,255,255,.28);
|
||||||
|
border-radius:999px; padding:3px 11px; font-size:12px; font-variant-numeric:tabular-nums;
|
||||||
|
}
|
||||||
|
h2{font-size:18px; margin:40px 0 6px; letter-spacing:-0.01em}
|
||||||
|
h2 .num{color:var(--muted); font-variant-numeric:tabular-nums; margin-right:8px}
|
||||||
|
h3{font-size:14px; margin:24px 0 8px; color:var(--text-secondary)}
|
||||||
|
p.lede{margin:0 0 16px; color:var(--text-secondary); font-size:13px}
|
||||||
|
code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:12.5px;
|
||||||
|
background:var(--surface-1); border:1px solid var(--border); border-radius:4px; padding:1px 5px}
|
||||||
|
.theme-toggle{
|
||||||
|
position:fixed; top:14px; right:16px; z-index:50; cursor:pointer;
|
||||||
|
background:var(--surface-1); color:var(--text-primary);
|
||||||
|
border:1px solid var(--border); border-radius:8px; padding:6px 12px; font-size:12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 결론 카드 ─────────────────────────────── */
|
||||||
|
.cards{display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:14px; margin:16px 0 8px}
|
||||||
|
.card{background:var(--surface-1); border:1px solid var(--border); border-radius:10px; padding:16px 18px}
|
||||||
|
.card .st{display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:600; margin-bottom:8px}
|
||||||
|
.card .st .ic{width:16px;height:16px;display:grid;place-items:center;border-radius:50%;color:#fff;font-size:11px;font-weight:700}
|
||||||
|
.card h4{margin:0 0 6px; font-size:14.5px}
|
||||||
|
.card p{margin:0 0 8px; font-size:12.5px; color:var(--text-secondary)}
|
||||||
|
.card .fix{font-size:12.5px; border-top:1px dashed var(--border); padding-top:8px; margin-top:4px}
|
||||||
|
.ic-crit{background:var(--st-crit)} .ic-warn{background:var(--st-warn); color:#0b0b0b} .ic-ser{background:var(--st-serious)}
|
||||||
|
|
||||||
|
/* ── 범례 ─────────────────────────────── */
|
||||||
|
.legend{display:flex; gap:18px; flex-wrap:wrap; align-items:center;
|
||||||
|
background:var(--surface-1); border:1px solid var(--border); border-radius:10px;
|
||||||
|
padding:11px 16px; margin:12px 0 18px; font-size:12.5px}
|
||||||
|
.legend .item{display:flex; align-items:center; gap:7px; color:var(--text-secondary)}
|
||||||
|
.legend svg{flex:none}
|
||||||
|
|
||||||
|
/* ── 격자 ─────────────────────────────── */
|
||||||
|
.grid{display:grid; gap:12px}
|
||||||
|
.g4{grid-template-columns:repeat(4,minmax(0,1fr))}
|
||||||
|
.g5{grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||||
|
.g3{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||||
|
@media(max-width:1080px){.g4,.g5{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||||
|
@media(max-width:640px){.g4,.g5,.g3{grid-template-columns:1fr}}
|
||||||
|
figure.cell{
|
||||||
|
margin:0; background:var(--surface-1); border:1px solid var(--border);
|
||||||
|
border-radius:10px; padding:10px 10px 8px; cursor:default; transition:border-color .12s;
|
||||||
|
}
|
||||||
|
figure.cell:hover{border-color:var(--series-1)}
|
||||||
|
figure.cell.ng{border-color:var(--st-crit)}
|
||||||
|
figure.cell svg{display:block; width:100%; height:auto}
|
||||||
|
figcaption{margin-top:6px}
|
||||||
|
figcaption .t{font-size:12.5px; font-weight:600; display:flex; align-items:center; gap:6px}
|
||||||
|
figcaption .s{font-size:11.5px; color:var(--muted); font-variant-numeric:tabular-nums; margin-top:2px}
|
||||||
|
.tag{font-size:10.5px; font-weight:700; padding:1px 6px; border-radius:4px; color:#fff; letter-spacing:.02em}
|
||||||
|
.tag.ng{background:var(--st-crit)} .tag.ok{background:var(--st-good)}
|
||||||
|
.tag.wr{background:var(--st-warn); color:#0b0b0b} .tag.na{background:var(--muted)}
|
||||||
|
|
||||||
|
/* 행 라벨 격자 */
|
||||||
|
.rowgrid{display:grid; gap:12px; grid-template-columns:132px repeat(4,minmax(0,1fr)); align-items:stretch}
|
||||||
|
@media(max-width:1080px){.rowgrid{grid-template-columns:1fr}}
|
||||||
|
.rowlab{display:flex; flex-direction:column; justify-content:center; font-size:12.5px;
|
||||||
|
font-weight:600; padding:4px 2px; border-right:1px solid var(--border)}
|
||||||
|
.rowlab span{font-weight:400; font-size:11.5px; color:var(--muted); margin-top:3px}
|
||||||
|
@media(max-width:1080px){.rowlab{border-right:none; border-bottom:1px solid var(--border); padding-bottom:8px}}
|
||||||
|
.colhead{display:grid; gap:12px; grid-template-columns:132px repeat(4,minmax(0,1fr));
|
||||||
|
font-size:12px; color:var(--text-secondary); font-weight:600; margin-bottom:8px}
|
||||||
|
@media(max-width:1080px){.colhead{display:none}}
|
||||||
|
|
||||||
|
/* ── 표 ─────────────────────────────── */
|
||||||
|
table{width:100%; border-collapse:collapse; font-size:12.5px; background:var(--surface-1);
|
||||||
|
border:1px solid var(--border); border-radius:10px; overflow:hidden; margin-top:10px}
|
||||||
|
th,td{padding:8px 11px; text-align:left; border-bottom:1px solid var(--grid); vertical-align:top}
|
||||||
|
th{background:var(--plane); font-weight:600; color:var(--text-secondary); font-size:12px}
|
||||||
|
td.num{font-variant-numeric:tabular-nums; white-space:nowrap}
|
||||||
|
tr:last-child td{border-bottom:none}
|
||||||
|
details{margin-top:12px} summary{cursor:pointer; font-size:13px; color:var(--text-secondary)}
|
||||||
|
|
||||||
|
/* ── 툴팁 ─────────────────────────────── */
|
||||||
|
#tip{position:fixed; z-index:80; pointer-events:none; opacity:0; transition:opacity .1s;
|
||||||
|
background:var(--surface-1); color:var(--text-primary); border:1px solid var(--border);
|
||||||
|
border-radius:8px; padding:9px 12px; font-size:12px; max-width:320px;
|
||||||
|
box-shadow:0 6px 22px rgba(0,0,0,.16); font-variant-numeric:tabular-nums}
|
||||||
|
#tip b{display:block; margin-bottom:4px; font-size:12.5px}
|
||||||
|
#tip .k{color:var(--muted)}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" id="themeBtn">🌗 다크/라이트</button>
|
||||||
|
|
||||||
|
<header class="banner"><div class="wrap">
|
||||||
|
<h1>Arc 출사 파라미터 시각화 — 원점 방향 이상 발광 분석</h1>
|
||||||
|
<p>ILSOS_PrismaticRD_PTOP · SeqUploadWeldingList / ScannerParam · 정적 분석 기준</p>
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge">작성 2026-09-07</span>
|
||||||
|
<span class="badge">근거 로그 Aurora_20260905 / 20260906</span>
|
||||||
|
<span class="badge">소스 무수정 (분석 전용)</span>
|
||||||
|
<span class="badge">현장 검증 미실시</span>
|
||||||
|
</div>
|
||||||
|
</div></header>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<!-- ══════════════ 0. 결론 ══════════════ -->
|
||||||
|
<h2><span class="num">0</span>결론 — Arc가 원점 방향으로 발광한 원인 후보 3건</h2>
|
||||||
|
<p class="lede">
|
||||||
|
모두 <b>레시피/스캐너 파라미터로 바로 회피 가능</b>합니다. 아래 카드의 조치값을 먼저 적용한 뒤 재현 여부를 보면
|
||||||
|
어느 후보인지 확정됩니다. 후보 1과 2는 Arc 전용 경로이고, 후보 3은 전 타입 공통입니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="st"><span class="ic ic-crit">✕</span><span style="color:var(--st-crit)">CRITICAL · 최유력</span></div>
|
||||||
|
<h4>후보 1 — Arc Ramping Interval = 0 (0으로 나눗셈)</h4>
|
||||||
|
<p><code>ArcRampingInterval</code>은 초기값이 없어 <b>기본 0.0</b>입니다(ScannerParam.cs:331).
|
||||||
|
여기에 Ramping Start/End Distance를 하나라도 넣으면 <code>theta = (0/R)×(180/π) = 0</code>이 되고,
|
||||||
|
<code>(int)(Dist/0)</code> → <code>(int)+∞</code> = <b>int.MinValue</b>로 카운터가 붕괴합니다.
|
||||||
|
Constant 루프가 <b>약 21.5억 회</b> <code>ArcAbs(center, 0°)</code>를 리스트에 밀어넣어
|
||||||
|
RTC 리스트가 넘치고, 넘친 자리의 미초기화 좌표(0,0 = 원점)가 <b>마크(레이저 ON)로 실행</b>됩니다.</p>
|
||||||
|
<div class="fix"><b>조치</b> · Arc를 쓰는 모든 레시피에서 <code>Arc Ramping Interval = 0.1 mm</code>
|
||||||
|
(툴팁 권장값). 램핑이 필요 없으면 <code>Start Distance = End Distance = 0</code>으로 두어
|
||||||
|
램핑 분기 자체를 타지 않게 하는 편이 더 안전합니다.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="st"><span class="ic ic-ser">!</span><span style="color:var(--st-serious)">SERIOUS</span></div>
|
||||||
|
<h4>후보 2 — 램핑 길이 가드의 단위 버그 → 과회전</h4>
|
||||||
|
<p>분기 조건(SeqUploadWeldingList.cs:1303)이 호 길이를 <code>R × |Sweep°|</code>로 계산합니다.
|
||||||
|
실제 호 길이는 <code>R × |Sweep°| × π/180</code>이라 <b>우변이 약 57.3배 과대</b>합니다.
|
||||||
|
(정확한 값은 <code>Arc.Length</code>에 이미 있는데 쓰지 않습니다.)
|
||||||
|
그래서 램핑 거리가 실제 호보다 길어도 분기에 진입하고,
|
||||||
|
rising+falling 반복이 totalCount를 넘겨 <b>의도한 각도를 초과해 레이저 ON 상태로 회전</b>합니다.
|
||||||
|
끝나면 <code>_LastX/_LastY</code>에 실제와 다른 좌표(PF2)를 기록해,
|
||||||
|
<b>다음 객체가 점프를 생략하고 엉뚱한 지점에서 마크를 시작</b>합니다.</p>
|
||||||
|
<div class="fix"><b>조치</b> · <code>Start Dist + End Dist ≤ 호길이 × 0.5</code>.
|
||||||
|
호길이 = <code>R × |Sweep°| × 0.01745</code>. §4에 계산 예시가 있습니다.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="st"><span class="ic ic-warn">!</span><span style="color:#8a6100">WARNING · 병행 확인</span></div>
|
||||||
|
<h4>후보 3 — LaserOnDelay −1800 µs + JumpDelay 0</h4>
|
||||||
|
<p><code>LaserOnDelay</code>는 <b>부호 있는 int</b>(ScannerParam.cs:58)이고 단위는 µs입니다
|
||||||
|
(ScannerRTC6.cs:691, 1bit=0.5µs). 음수는 마크 시작 <b>이전에</b> 레이저를 켜므로
|
||||||
|
<b>직전 점프 구간 끝에서 발광</b>합니다. 현장 로그 실측값이 −1800 µs이고 JumpDelay가 0이라
|
||||||
|
미러 정정 대기 없이 발광합니다. JumpSpeed 1000 mm/s 기준 <b>최대 1.8 mm 선행 발광</b>.</p>
|
||||||
|
<div class="fix"><b>조치</b> · <code>LaserOnDelay</code> −1800 → −200~0 검토,
|
||||||
|
<code>JumpDelay</code> 0 → 100~300 µs.
|
||||||
|
<b>단 1.8 mm 규모라 "원점까지"는 이것만으로 설명되지 않습니다.</b> 후보 1·2와 함께 보십시오.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════════ 1. 기준 파라미터 ══════════════ -->
|
||||||
|
<h2><span class="num">1</span>기준 파라미터 세트 (현장 로그 실측값)</h2>
|
||||||
|
<p class="lede">
|
||||||
|
아래 A는 <code>Aurora_20260906.log</code>에서 그대로 읽은 값이고, B·C는 이 문서의 시뮬레이션 예시입니다.
|
||||||
|
이후 모든 격자는 이 세 세트를 기준으로 그렸습니다.
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr>
|
||||||
|
<th style="width:26%">파라미터</th><th style="width:24%">A · 현장 실측</th>
|
||||||
|
<th style="width:24%">B · 권장(램핑 사용)</th><th style="width:26%">C · 재현 조건(NG)</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>LaserOnDelay</td><td class="num">−1800 µs</td><td class="num">−200 µs</td><td class="num">−1800 µs</td></tr>
|
||||||
|
<tr><td>LaserOffDelay <span style="color:var(--muted)">(uint·음수 불가)</span></td><td class="num">0 µs</td><td class="num">0 µs</td><td class="num">0 µs</td></tr>
|
||||||
|
<tr><td>JumpDelay</td><td class="num">0 µs</td><td class="num">200 µs</td><td class="num">0 µs</td></tr>
|
||||||
|
<tr><td>MarkDelay</td><td class="num">200 µs</td><td class="num">200 µs</td><td class="num">200 µs</td></tr>
|
||||||
|
<tr><td>PolygonDelay <span style="color:var(--muted)">(Arc 분절 간)</span></td><td class="num">0 µs</td><td class="num">50~100 µs</td><td class="num">0 µs</td></tr>
|
||||||
|
<tr><td>JumpSpeed</td><td class="num">1000 mm/s</td><td class="num">1000 mm/s</td><td class="num">1000 mm/s</td></tr>
|
||||||
|
<tr><td>MarkSpeed</td><td class="num">100 mm/s</td><td class="num">100 mm/s</td><td class="num">100 mm/s</td></tr>
|
||||||
|
<tr><td>LaserPower1 (Center)</td><td class="num">467 W</td><td class="num">467 W</td><td class="num">467 W</td></tr>
|
||||||
|
<tr><td><b>ArcRampingInterval</b></td><td class="num">— (미설정 = 0)</td><td class="num"><b>0.1 mm</b></td><td class="num"><b>0 mm</b></td></tr>
|
||||||
|
<tr><td>RampingStartDist / EndDist</td><td class="num">0 / 0</td><td class="num">2 / 2 mm</td><td class="num">10 / 10 mm</td></tr>
|
||||||
|
<tr><td>RampingStartPower / EndPower</td><td class="num">—</td><td class="num">30 / 30 %</td><td class="num">30 / 30 %</td></tr>
|
||||||
|
<tr><td>Arc 형상 (예시)</td><td class="num">R 10 mm, Sweep 90°</td><td class="num">R 10 mm, Sweep 90°</td><td class="num">R 10 mm, Sweep 90°</td></tr>
|
||||||
|
<tr><td style="color:var(--muted)">→ 실제 호 길이</td><td class="num" colspan="3">R × Sweep° × π/180 = 10 × 90 × 0.01745 = <b>15.71 mm</b></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- ══════════════ 2. 범례 ══════════════ -->
|
||||||
|
<h2><span class="num">2</span>격자 읽는 법</h2>
|
||||||
|
<div class="legend" id="legend"></div>
|
||||||
|
<p class="lede" style="margin-top:-6px">
|
||||||
|
색만으로 구분하지 않습니다 — <b>점프는 파선, 마크는 실선, 이상 발광은 굵은 실선 + ✕ 표식</b>입니다.
|
||||||
|
파워 크기는 같은 파랑 계열의 명도(연함 = 저출력, 진함 = 고출력)로만 표현합니다.
|
||||||
|
각 셀에 마우스를 올리면 해당 조합의 파라미터가 표시됩니다. 전체 수치는 §6 표로도 제공합니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- ══════════════ 3. 타입별 ══════════════ -->
|
||||||
|
<h2><span class="num">3</span>격자 A — Draw 타입별 출사 형상 (출사 분기 8종)</h2>
|
||||||
|
<p class="lede">
|
||||||
|
<code>FuncLoadDrawObjectScannerParamSelected()</code>가 실제로 처리하는 8개 분기입니다.
|
||||||
|
Ellipse · Rectangle · Roi는 분기가 없어 여기에 없습니다(별건 — 예외처리 대기 중).
|
||||||
|
</p>
|
||||||
|
<div class="grid g4" id="gridA"></div>
|
||||||
|
|
||||||
|
<!-- ══════════════ 4. Arc 조합 ══════════════ -->
|
||||||
|
<h2><span class="num">4</span>격자 B — Arc × 램핑/와블/스노우맨 조합 <span style="color:var(--st-crit)">(핵심)</span></h2>
|
||||||
|
<p class="lede">
|
||||||
|
행 = 램핑 상태, 열 = 와블/스노우맨 상태. 모두 <b>R 10 mm · Sweep 90° · CCW</b> 동일 형상입니다.
|
||||||
|
빨간 셀이 이번 현상의 재현 조건입니다.
|
||||||
|
</p>
|
||||||
|
<div class="colhead">
|
||||||
|
<div></div><div>Wobble OFF</div><div>Wobble ON · 진폭 小</div><div>Wobble ON · 진폭 大</div><div>SnowMan ON</div>
|
||||||
|
</div>
|
||||||
|
<div id="gridB"></div>
|
||||||
|
|
||||||
|
<!-- ══════════════ 5. 값 크기 ══════════════ -->
|
||||||
|
<h2><span class="num">5</span>격자 C — 값의 크고 작음이 만드는 차이</h2>
|
||||||
|
<p class="lede">각 행은 파라미터 하나를 <b>작게 / 권장 / 크게</b> 3단으로 비교합니다.</p>
|
||||||
|
<div class="colhead" style="grid-template-columns:132px repeat(3,minmax(0,1fr))">
|
||||||
|
<div></div><div>작게</div><div>권장</div><div>크게</div>
|
||||||
|
</div>
|
||||||
|
<div id="gridC"></div>
|
||||||
|
|
||||||
|
<!-- ══════════════ 6. 표 ══════════════ -->
|
||||||
|
<h2><span class="num">6</span>표 뷰 — 격자 B 전체 수치</h2>
|
||||||
|
<p class="lede">그림 없이도 같은 내용을 읽을 수 있도록 격자 B를 표로 옮겼습니다.</p>
|
||||||
|
<table id="tblB"><thead><tr>
|
||||||
|
<th>램핑 상태</th><th>와블 상태</th><th class="num">Interval</th><th class="num">Start+End</th>
|
||||||
|
<th class="num">theta</th><th class="num">totalCount</th><th class="num">실제 회전각</th><th>판정</th><th>근거</th>
|
||||||
|
</tr></thead><tbody></tbody></table>
|
||||||
|
|
||||||
|
<!-- ══════════════ 7. 체크리스트 ══════════════ -->
|
||||||
|
<h2><span class="num">7</span>현장 조치 체크리스트</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th style="width:34px">#</th><th style="width:30%">항목</th><th>내용</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="num">1</td><td>Arc Ramping Interval 확인</td>
|
||||||
|
<td>Arc를 쓰는 <b>모든</b> 레시피 · 모든 Scanner Param No에 대해 값이 <code>0</code>이 아닌지 확인.
|
||||||
|
0이면 <b>0.1</b>로 설정. (램핑 미사용이면 2번으로)</td></tr>
|
||||||
|
<tr><td class="num">2</td><td>램핑 미사용 시</td>
|
||||||
|
<td><code>Start Distance = 0</code>, <code>End Distance = 0</code> → 단일 <code>ArcAbs</code> 경로(:1369)로 빠져
|
||||||
|
카운터 붕괴·과회전 모두 회피됩니다. <b>가장 안전한 설정</b>입니다.</td></tr>
|
||||||
|
<tr><td class="num">3</td><td>램핑 사용 시 길이 검산</td>
|
||||||
|
<td>호길이 = <code>R × Sweep° × 0.01745</code>. <code>Start + End ≤ 호길이 × 0.5</code>를 만족하는지
|
||||||
|
Arc마다 확인. 코드의 자동 가드는 57.3배 느슨해서 <b>걸러주지 않습니다</b>.</td></tr>
|
||||||
|
<tr><td class="num">4</td><td>선행 발광 확인</td>
|
||||||
|
<td><code>LaserOnDelay</code> −1800 µs 유지 여부 판단. 점프 종단 1.8 mm 발광이 허용되는 공정인지 확인.
|
||||||
|
줄인다면 −200 µs부터. 동시에 <code>JumpDelay</code>를 100~300 µs로.</td></tr>
|
||||||
|
<tr><td class="num">5</td><td>Arc 분절 정정</td>
|
||||||
|
<td><code>PolygonDelay = 0</code>은 Arc 분절 사이 정정 대기가 없다는 뜻입니다.
|
||||||
|
원호 오버슈트가 보이면 50~100 µs를 넣고 비교.</td></tr>
|
||||||
|
<tr><td class="num">6</td><td>재현 시험</td>
|
||||||
|
<td>1·2번만 적용하고 재현되는지 먼저 확인 → 재현되면 후보 3(딜레이) 쪽으로 이동.
|
||||||
|
한 번에 다 바꾸면 어느 것이 원인인지 확정되지 않습니다.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>참조한 소스 위치</summary>
|
||||||
|
<table style="margin-top:8px">
|
||||||
|
<thead><tr><th style="width:44%">위치</th><th>내용</th></tr></thead><tbody>
|
||||||
|
<tr><td><code>SeqUploadWeldingList.cs:1269</code></td><td><code>length = param.ArcRampingInterval</code></td></tr>
|
||||||
|
<tr><td><code>SeqUploadWeldingList.cs:1273-1278</code></td><td><code>theta = (length/radius)×(180/π)</code>, CW는 부호 반전</td></tr>
|
||||||
|
<tr><td><code>SeqUploadWeldingList.cs:1302-1303</code></td><td>램핑 분기 가드 — <code>R×|Sweep°|</code> (π/180 누락)</td></tr>
|
||||||
|
<tr><td><code>SeqUploadWeldingList.cs:1313-1316</code></td><td><code>risingEdgeCount</code>·<code>fallingEdgeCount</code>·<code>totalCount</code></td></tr>
|
||||||
|
<tr><td><code>SeqUploadWeldingList.cs:1342-1344</code></td><td>Constant 구간 루프 (카운터 붕괴 시 폭주 지점)</td></tr>
|
||||||
|
<tr><td><code>SeqUploadWeldingList.cs:1366-1367</code></td><td><code>lastSweep</code> 보정 <code>ArcAbs</code></td></tr>
|
||||||
|
<tr><td><code>SeqUploadWeldingList.cs:1374-1375</code></td><td><code>_LastX/_LastY = obj.PF2</code> (물리 위치와 어긋날 수 있음)</td></tr>
|
||||||
|
<tr><td><code>SeqUploadWeldingList.cs:1220, 800</code></td><td>"이미 시작점인가" 판정 → 점프 생략 분기</td></tr>
|
||||||
|
<tr><td><code>ScannerParam.cs:331-341</code></td><td><code>ArcRampingInterval</code> — 초기값 없음(=0), 권장 0.1 mm</td></tr>
|
||||||
|
<tr><td><code>ScannerParam.cs:56-70</code></td><td><code>LaserOnDelay</code>=int(음수 가능), <code>LaserOffDelay</code>=uint</td></tr>
|
||||||
|
<tr><td><code>ScannerRTC6.cs:691-699</code></td><td>딜레이 단위 — 레이저 0.5 µs/bit, 스캐너 10 µs/bit</td></tr>
|
||||||
|
<tr><td><code>ScannerRTC6.cs:262-271</code></td><td><code>ArcAbs(x,y,angle)</code> — 각도 degree, CW가 +</td></tr>
|
||||||
|
<tr><td><code>ArcCircleTool.cs:905-924</code></td><td><code>SweepAngle</code> degree, <code>Length = R×Sweep×π/180</code> (정확값 존재)</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tip"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* ═══════════ 테마 ═══════════ */
|
||||||
|
document.getElementById('themeBtn').onclick = () => {
|
||||||
|
const r = document.documentElement;
|
||||||
|
r.dataset.theme = r.dataset.theme === 'dark' ? 'light' : 'dark';
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ═══════════ 툴팁 ═══════════ */
|
||||||
|
const tip = document.getElementById('tip');
|
||||||
|
document.addEventListener('mousemove', e => {
|
||||||
|
const c = e.target.closest('[data-tip]');
|
||||||
|
if (!c) { tip.style.opacity = 0; return; }
|
||||||
|
tip.innerHTML = c.dataset.tip;
|
||||||
|
tip.style.opacity = 1;
|
||||||
|
const r = tip.getBoundingClientRect();
|
||||||
|
let x = e.clientX + 16, y = e.clientY + 16;
|
||||||
|
if (x + r.width > innerWidth - 8) x = e.clientX - r.width - 16;
|
||||||
|
if (y + r.height > innerHeight - 8) y = e.clientY - r.height - 16;
|
||||||
|
tip.style.left = x + 'px'; tip.style.top = y + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ═══════════ 기하 헬퍼 ═══════════ */
|
||||||
|
const W = 236, H = 168; // 셀 viewBox
|
||||||
|
const ORIG = [26, 142]; // 필드 원점 (0,0)
|
||||||
|
const CX = 140, CY = 74, R = 40; // 기본 Arc 중심/반경
|
||||||
|
const D2R = Math.PI / 180;
|
||||||
|
|
||||||
|
const P = (cx, cy, r, deg) => [cx + r * Math.cos(deg * D2R), cy - r * Math.sin(deg * D2R)];
|
||||||
|
const fmt = a => a.map(v => v.toFixed(2)).join(' ');
|
||||||
|
|
||||||
|
function poly(pts) { return pts.map((p, i) => (i ? 'L' : 'M') + fmt(p)).join(' '); }
|
||||||
|
|
||||||
|
function arcPts(cx, cy, r, a0, a1, n) {
|
||||||
|
n = n || Math.max(10, Math.ceil(Math.abs(a1 - a0) / 2));
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i <= n; i++) out.push(P(cx, cy, r, a0 + (a1 - a0) * i / n));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 와블: shape 0=ELLIPSE, 1=HORIZONTAL8, -1=VERTICAL8 */
|
||||||
|
function wobbleArcPts(cx, cy, r, a0, a1, ampT, ampL, cycles, shape) {
|
||||||
|
const n = 900, out = [];
|
||||||
|
for (let i = 0; i <= n; i++) {
|
||||||
|
const t = i / n, u = t * cycles * 2 * Math.PI, a = a0 + (a1 - a0) * t;
|
||||||
|
let dR, dA;
|
||||||
|
if (shape === 0) { dR = ampT * Math.sin(u); dA = ampL * Math.cos(u); }
|
||||||
|
else if (shape === 1) { dR = ampT * Math.sin(u); dA = ampL * Math.sin(2 * u); }
|
||||||
|
else { dR = ampT * Math.sin(2 * u); dA = ampL * Math.sin(u); }
|
||||||
|
const rr = r + dR, aa = a + (dA / Math.max(rr, 1)) * (180 / Math.PI);
|
||||||
|
out.push(P(cx, cy, rr, aa));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function wobbleLinePts(x0, y0, x1, y1, ampT, ampL, cycles, shape) {
|
||||||
|
const n = 700, out = [], dx = x1 - x0, dy = y1 - y0;
|
||||||
|
const L = Math.hypot(dx, dy), ux = dx / L, uy = dy / L, nx = -uy, ny = ux;
|
||||||
|
for (let i = 0; i <= n; i++) {
|
||||||
|
const t = i / n, u = t * cycles * 2 * Math.PI;
|
||||||
|
let dT, dL;
|
||||||
|
if (shape === 0) { dT = ampT * Math.sin(u); dL = ampL * Math.cos(u); }
|
||||||
|
else if (shape === 1) { dT = ampT * Math.sin(u); dL = ampL * Math.sin(2 * u); }
|
||||||
|
else { dT = ampT * Math.sin(2 * u); dL = ampL * Math.sin(u); }
|
||||||
|
out.push([x0 + ux * (t * L + dL) + nx * dT, y0 + uy * (t * L + dL) + ny * dT]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════ SVG 조각 ═══════════ */
|
||||||
|
const s = (t, at, inner) => `<${t} ${Object.entries(at).map(([k, v]) => `${k}="${v}"`).join(' ')}${inner === undefined ? '/>' : `>${inner}</${t}>`}`;
|
||||||
|
|
||||||
|
const MARK = d => s('path', { d, fill: 'none', stroke: 'var(--series-1)', 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' });
|
||||||
|
const JUMP = d => s('path', { d, fill: 'none', stroke: 'var(--series-2)', 'stroke-width': 1.6, 'stroke-dasharray': '4 3', 'stroke-linecap': 'round' });
|
||||||
|
const BAD = d => s('path', { d, fill: 'none', stroke: 'var(--st-crit)', 'stroke-width': 3, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' });
|
||||||
|
const WOB = d => s('path', { d, fill: 'none', stroke: 'var(--series-3)', 'stroke-width': 1.2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' });
|
||||||
|
const PWR = (d, step) => s('path', { d, fill: 'none', stroke: `var(--p${step})`, 'stroke-width': 3.2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' });
|
||||||
|
|
||||||
|
function frame(label) {
|
||||||
|
let g = s('rect', { x: 8, y: 8, width: W - 16, height: H - 16, rx: 6, fill: 'none', stroke: 'var(--grid)', 'stroke-width': 1 });
|
||||||
|
// 원점 크로스
|
||||||
|
g += s('line', { x1: ORIG[0] - 7, y1: ORIG[1], x2: ORIG[0] + 7, y2: ORIG[1], stroke: 'var(--axis)', 'stroke-width': 1.4 });
|
||||||
|
g += s('line', { x1: ORIG[0], y1: ORIG[1] - 7, x2: ORIG[0], y2: ORIG[1] + 7, stroke: 'var(--axis)', 'stroke-width': 1.4 });
|
||||||
|
g += s('text', { x: ORIG[0] + 10, y: ORIG[1] + 4, fill: 'var(--muted)', 'font-size': 9 }, label || '원점(0,0)');
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
function dot(p, color) { return s('circle', { cx: p[0].toFixed(2), cy: p[1].toFixed(2), r: 3, fill: color, stroke: 'var(--surface-1)', 'stroke-width': 2 }); }
|
||||||
|
function xmark(p) {
|
||||||
|
const [x, y] = p, k = 4.5;
|
||||||
|
return s('g', { stroke: 'var(--st-crit)', 'stroke-width': 2.2, 'stroke-linecap': 'round' },
|
||||||
|
s('line', { x1: x - k, y1: y - k, x2: x + k, y2: y + k }) + s('line', { x1: x - k, y1: y + k, x2: x + k, y2: y - k }));
|
||||||
|
}
|
||||||
|
function note(x, y, t, color) { return s('text', { x, y, fill: color || 'var(--muted)', 'font-size': 9.5 }, t); }
|
||||||
|
|
||||||
|
/* ═══════════ 셀 빌더 ═══════════ */
|
||||||
|
function cell(spec) {
|
||||||
|
const tag = spec.tag ? `<span class="tag ${spec.tag[0]}">${spec.tag[1]}</span>` : '';
|
||||||
|
return `<figure class="cell${spec.ng ? ' ng' : ''}" data-tip="${spec.tip.replace(/"/g, '"')}">
|
||||||
|
<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${spec.title}">${frame(spec.originLabel)}${spec.body}</svg>
|
||||||
|
<figcaption><div class="t">${spec.title}${tag}</div><div class="s">${spec.sub}</div></figcaption>
|
||||||
|
</figure>`;
|
||||||
|
}
|
||||||
|
const TIP = (title, rows) => `<b>${title}</b>` + rows.map(([k, v]) => `<span class="k">${k}</span> ${v}`).join('<br>');
|
||||||
|
|
||||||
|
/* ═══════════ 범례 ═══════════ */
|
||||||
|
document.getElementById('legend').innerHTML = [
|
||||||
|
['<svg width="34" height="10"><line x1="1" y1="5" x2="33" y2="5" stroke="var(--series-1)" stroke-width="2" stroke-linecap="round"/></svg>', '마크 = 용접 (레이저 ON, 의도됨)'],
|
||||||
|
['<svg width="34" height="10"><line x1="1" y1="5" x2="33" y2="5" stroke="var(--series-2)" stroke-width="1.6" stroke-dasharray="4 3" stroke-linecap="round"/></svg>', '점프 (레이저 OFF, 의도됨)'],
|
||||||
|
['<svg width="34" height="10"><line x1="1" y1="5" x2="33" y2="5" stroke="var(--st-crit)" stroke-width="3" stroke-linecap="round"/></svg>', '✕ 이상 발광 (레이저 ON, 의도 안 됨)'],
|
||||||
|
['<svg width="34" height="10"><path d="M1 5 Q5 1 9 5 T17 5 T25 5 T33 5" fill="none" stroke="var(--series-3)" stroke-width="1.2"/></svg>', '와블 궤적'],
|
||||||
|
['<svg width="52" height="10"><line x1="1" y1="5" x2="16" y2="5" stroke="var(--p250)" stroke-width="3.2" stroke-linecap="round"/><line x1="19" y1="5" x2="33" y2="5" stroke="var(--p450)" stroke-width="3.2" stroke-linecap="round"/><line x1="36" y1="5" x2="51" y2="5" stroke="var(--p650)" stroke-width="3.2" stroke-linecap="round"/></svg>', '파워 저 → 고'],
|
||||||
|
].map(([sv, t]) => `<span class="item">${sv}<span>${t}</span></span>`).join('');
|
||||||
|
|
||||||
|
/* ═══════════ 격자 A — 타입별 ═══════════ */
|
||||||
|
const A = [];
|
||||||
|
const jumpTo = p => JUMP(poly([ORIG, p]));
|
||||||
|
|
||||||
|
// 1 Line
|
||||||
|
{ const a = [80, 110], b = [200, 52];
|
||||||
|
A.push({ title: 'Line', sub: 'LoadLineScannerParam · :696', tag: ['ok', '지원'],
|
||||||
|
body: jumpTo(a) + MARK(poly([a, b])) + dot(a, 'var(--series-2)') + dot(b, 'var(--series-1)'),
|
||||||
|
tip: TIP('Line', [['경로', '점프 → 직선 마크 1회'], ['램핑', '지원 (거리 기반 분할)'], ['와블', '지원'], ['비고', 'PolyLine(LineEdit)도 이 분기']]) }); }
|
||||||
|
|
||||||
|
// 2 Dot
|
||||||
|
{ const a = [150, 84];
|
||||||
|
A.push({ title: 'Dot', sub: 'LoadDotScannerParam · :986', tag: ['ok', '지원'],
|
||||||
|
body: jumpTo(a) + s('circle', { cx: a[0], cy: a[1], r: 5.5, fill: 'none', stroke: 'var(--series-1)', 'stroke-width': 2.4 }) + dot(a, 'var(--series-1)') + note(a[0] + 12, a[1] + 4, 'SpotShotTime'),
|
||||||
|
tip: TIP('Dot', [['경로', '점프 → 정지 발광'], ['시간', 'SpotShotTime'], ['램핑', '해당 없음'], ['와블', '해당 없음']]) }); }
|
||||||
|
|
||||||
|
// 3 Spline
|
||||||
|
{ const pts = [];
|
||||||
|
for (let i = 0; i <= 120; i++) { const t = i / 120, ang = t * 720, r = 12 + t * 30; pts.push(P(CX, CY + 6, r, ang)); }
|
||||||
|
A.push({ title: 'Spline', sub: 'LoadSplineScannerParam · :1032', tag: ['ok', '지원'],
|
||||||
|
body: jumpTo(pts[0]) + MARK(poly(pts)) + dot(pts[0], 'var(--series-2)'),
|
||||||
|
tip: TIP('Spline', [['경로', '나선(토네이도) 마크'], ['방향', 'DrawWayType 4종'], ['램핑', '미적용'], ['와블', '미적용']]) }); }
|
||||||
|
|
||||||
|
// 4 SnowMan
|
||||||
|
{ const a = [86, 104], b = [190, 62];
|
||||||
|
const w = wobbleLinePts(a[0], a[1], b[0], b[1], 7, 3, 7, 1);
|
||||||
|
A.push({ title: 'SnowMan', sub: 'LoadSnowManScannerParam · :1132', tag: ['wr', '_Last 미갱신'],
|
||||||
|
body: jumpTo(a) + WOB(poly(w)) + MARK(poly([a, b])) + dot(a, 'var(--series-2)') + dot(b, 'var(--series-1)') + note(12, 24, '⚠ _LastX/_LastY 갱신 없음 (:1179)', 'var(--st-serious)'),
|
||||||
|
tip: TIP('SnowMan', [['경로', '와블 벡터(Head/Body) + 직선'], ['주의', '_LastX/_LastY를 갱신하지 않음'], ['영향', '다음 객체의 점프 생략 판정이 stale 좌표로'], ['현황', '현장 미사용']]) }); }
|
||||||
|
|
||||||
|
// 5 Arc
|
||||||
|
{ const pts = arcPts(CX, CY, R, 200, 340);
|
||||||
|
A.push({ title: 'Arc', sub: 'LoadArcScannerParam · :1183', tag: ['ok', '지원'],
|
||||||
|
body: jumpTo(pts[0]) + MARK(poly(pts)) + dot(pts[0], 'var(--series-2)') + dot(pts[pts.length - 1], 'var(--series-1)') + dot([CX, CY], 'var(--muted)') + note(CX - 12, CY - 8, 'center'),
|
||||||
|
tip: TIP('Arc', [['경로', '현재 위치에서 center 기준 Sweep° 회전'], ['포함', 'Circle (Arc 파생)'], ['램핑', '지원 — 본 문서의 핵심'], ['와블/스노우맨', '모두 지원']]) }); }
|
||||||
|
|
||||||
|
// 6 Arc3Point
|
||||||
|
{ const pts = arcPts(CX, CY + 10, R, 195, 345);
|
||||||
|
const p3 = [arcPts(CX, CY + 10, R, 270, 270)[0]];
|
||||||
|
A.push({ title: 'Arc3Point', sub: 'LoadArc3PScannerParam · :1384', tag: ['ok', '지원'],
|
||||||
|
body: jumpTo(pts[0]) + MARK(poly(pts)) + dot(pts[0], 'var(--series-2)') + dot(p3[0], 'var(--muted)') + dot(pts[pts.length - 1], 'var(--series-1)') + note(10, 24, '3점 통과 원호'),
|
||||||
|
tip: TIP('Arc3Point', [['경로', '3점을 지나는 원호'], ['키', 'Arc3P123 / Arc3P132'], ['직렬화명', 'Arc3p'], ['램핑', 'Arc와 별도 구현']]) }); }
|
||||||
|
|
||||||
|
// 7 ZigZag
|
||||||
|
{ const a = [76, 112], b = [198, 58], zz = [];
|
||||||
|
const n = 7;
|
||||||
|
for (let i = 0; i <= n; i++) {
|
||||||
|
const t = i / n, x = a[0] + (b[0] - a[0]) * t, y = a[1] + (b[1] - a[1]) * t;
|
||||||
|
zz.push([x, y + (i % 2 ? -13 : 13)]);
|
||||||
|
}
|
||||||
|
A.push({ title: 'ZigZag', sub: 'LoadZigzagScannerParam · :1585', tag: ['ok', '지원'],
|
||||||
|
body: jumpTo(zz[0]) + MARK(poly(zz)) + dot(zz[0], 'var(--series-2)') + dot(zz[zz.length - 1], 'var(--series-1)'),
|
||||||
|
tip: TIP('ZigZag', [['경로', '직선 + 원호 반복'], ['파라미터', 'ZigzagWobbel* 계열'], ['램핑', '미적용']]) }); }
|
||||||
|
|
||||||
|
// 8 Group
|
||||||
|
{ const l1 = [[70, 118], [136, 92]], l2 = [[136, 92], [196, 106]];
|
||||||
|
const ar = arcPts(150, 60, 26, 200, 350);
|
||||||
|
A.push({ title: 'Group', sub: 'LoadGroupScannerParam · :337', tag: ['ok', '지원'],
|
||||||
|
body: jumpTo(l1[0]) + MARK(poly(l1)) + MARK(poly(l2)) + JUMP(poly([l2[1], ar[0]])) + MARK(poly(ar)) + dot(l1[0], 'var(--series-2)') + note(10, 24, '부모 ScannerParam 상속 · MarkingCount 반복'),
|
||||||
|
tip: TIP('Group', [['경로', '서브객체를 순서대로 재귀 처리'], ['조건', '부모 객체의 ScannerParam 사용'], ['반복', 'MarkingCount 회'], ['램핑', 'GroupRamingUse=true 시 전체 길이 기준 램핑']]) }); }
|
||||||
|
|
||||||
|
document.getElementById('gridA').innerHTML = A.map(cell).join('');
|
||||||
|
|
||||||
|
/* ═══════════ 격자 B — Arc 조합 ═══════════ */
|
||||||
|
const A0 = 200, A1 = 290; // 90° 스윕 (화면상)
|
||||||
|
const arcBase = arcPts(CX, CY, R, A0, A1);
|
||||||
|
const START = arcBase[0], END = arcBase[arcBase.length - 1];
|
||||||
|
|
||||||
|
/* 램핑 구간을 파워 계단으로 그린다 */
|
||||||
|
function rampingBody(startFrac, endFrac, steps) {
|
||||||
|
const span = A1 - A0, out = [];
|
||||||
|
const n = steps || 5;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const t0 = i / n, t1 = (i + 1) / n;
|
||||||
|
let lvl;
|
||||||
|
if (t1 <= startFrac) lvl = 250 + Math.round((t0 / Math.max(startFrac, 1e-6)) * 3) * 100;
|
||||||
|
else if (t0 >= 1 - endFrac) lvl = 650 - Math.round(((t0 - (1 - endFrac)) / Math.max(endFrac, 1e-6)) * 3) * 100;
|
||||||
|
else lvl = 650;
|
||||||
|
lvl = Math.max(250, Math.min(650, lvl));
|
||||||
|
out.push(PWR(poly(arcPts(CX, CY, R, A0 + span * t0, A0 + span * t1, 14)), lvl));
|
||||||
|
}
|
||||||
|
return out.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const wobSmall = () => WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 3, 1.5, 14, 0)));
|
||||||
|
const wobLarge = () => WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 9, 5, 9, 0)));
|
||||||
|
const snowMan = () => WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 7, 4, 11, 1)));
|
||||||
|
|
||||||
|
const rowsB = [
|
||||||
|
{ key: 'off', lab: '램핑 OFF', sub: 'Start=0 · End=0', interval: '—', se: '0 mm', theta: '—', total: '—', real: '90.0°', verdict: ['ok', 'OK'], why: '단일 ArcAbs (:1369)' },
|
||||||
|
{ key: 'ok', lab: '램핑 ON · 정상', sub: 'Interval 0.1 · 2+2 mm', interval: '0.1 mm', se: '4 mm', theta: '0.573°', total: '157', real: '90.0°', verdict: ['ok', 'OK'], why: 'rising 20 + const 117 + falling 20' },
|
||||||
|
{ key: 'over', lab: '램핑 ON · 거리 과대', sub: 'Interval 0.1 · 10+10 mm', interval: '0.1 mm', se: '20 mm', theta: '0.573°', total: '157', real: '≈114.6°', verdict: ['ng', '과회전'], why: 'rising100 + const0 + falling100 → 24.6° 초과' },
|
||||||
|
{ key: 'zero', lab: '램핑 ON · Interval 0', sub: 'Interval 0 · 10+10 mm', interval: '0 mm', se: '20 mm', theta: '0°', total: 'int.MinValue', real: '붕괴', verdict: ['ng', '리스트 폭주'], why: '0으로 나눗셈 → 21.5억 회 ArcAbs' },
|
||||||
|
];
|
||||||
|
const colsB = [
|
||||||
|
{ key: 'woff', lab: 'Wobble OFF', ov: () => '', p: 'WobbelEnable=false' },
|
||||||
|
{ key: 'wsm', lab: 'Wobble ON · 진폭 小', ov: wobSmall, p: 'Trans 0.3 / Long 0.15 mm · 150 Hz' },
|
||||||
|
{ key: 'wlg', lab: 'Wobble ON · 진폭 大', ov: wobLarge, p: 'Trans 1.5 / Long 0.8 mm · 100 Hz' },
|
||||||
|
{ key: 'snow', lab: 'SnowMan ON', ov: snowMan, p: 'SnowManEnable=true · Tilting 45°' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let htmlB = '';
|
||||||
|
for (const r of rowsB) {
|
||||||
|
htmlB += `<div class="rowgrid" style="margin-bottom:12px">
|
||||||
|
<div class="rowlab">${r.lab}<span>${r.sub}</span></div>`;
|
||||||
|
for (const c of colsB) {
|
||||||
|
let body = '', ng = false, tag = r.verdict;
|
||||||
|
if (r.key === 'off') {
|
||||||
|
body = JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + c.ov();
|
||||||
|
} else if (r.key === 'ok') {
|
||||||
|
body = JUMP(poly([ORIG, START])) + rampingBody(2 / 15.71, 2 / 15.71, 8) + c.ov();
|
||||||
|
} else if (r.key === 'over') {
|
||||||
|
ng = true;
|
||||||
|
const overPts = arcPts(CX, CY, R, A1, A1 + 24.6);
|
||||||
|
body = JUMP(poly([ORIG, START])) + rampingBody(0.42, 0.42, 8) + c.ov()
|
||||||
|
+ BAD(poly(overPts)) + xmark(overPts[overPts.length - 1])
|
||||||
|
+ note(10, 24, '✕ 90° → 114.6° 초과 회전', 'var(--st-crit)')
|
||||||
|
+ note(10, 36, '_Last는 90° 지점으로 기록 → 다음 객체 오작동', 'var(--st-serious)');
|
||||||
|
} else {
|
||||||
|
ng = true;
|
||||||
|
body = JUMP(poly([ORIG, START]))
|
||||||
|
+ BAD(poly([END, [ORIG[0], ORIG[1]]]))
|
||||||
|
+ MARK(poly(arcBase)) + c.ov()
|
||||||
|
+ xmark(ORIG)
|
||||||
|
+ note(10, 24, '✕ 리스트 오버플로 → 원점(0,0)으로 마크', 'var(--st-crit)')
|
||||||
|
+ note(10, 36, '레이저 ON 상태로 원점 복귀 = 현장 현상', 'var(--st-crit)');
|
||||||
|
}
|
||||||
|
htmlB += cell({
|
||||||
|
title: c.lab, sub: `${r.sub}`, tag, ng, body,
|
||||||
|
tip: TIP(`${r.lab} × ${c.lab}`, [
|
||||||
|
['Arc', 'R 10 mm · Sweep 90° · CCW · 호길이 15.71 mm'],
|
||||||
|
['Interval', r.interval], ['Start+End', r.se],
|
||||||
|
['theta', r.theta], ['totalCount', r.total],
|
||||||
|
['실제 회전각', r.real], ['와블', c.p], ['판정 근거', r.why]])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
htmlB += `</div>`;
|
||||||
|
}
|
||||||
|
document.getElementById('gridB').innerHTML = htmlB;
|
||||||
|
|
||||||
|
/* 표 뷰 */
|
||||||
|
document.querySelector('#tblB tbody').innerHTML = rowsB.flatMap(r => colsB.map(c => `<tr>
|
||||||
|
<td>${r.lab}</td><td>${c.lab}</td><td class="num">${r.interval}</td><td class="num">${r.se}</td>
|
||||||
|
<td class="num">${r.theta}</td><td class="num">${r.total}</td><td class="num">${r.real}</td>
|
||||||
|
<td><span class="tag ${r.verdict[0]}">${r.verdict[1]}</span></td><td>${r.why}</td></tr>`)).join('');
|
||||||
|
|
||||||
|
/* ═══════════ 격자 C — 값 크기 ═══════════ */
|
||||||
|
const rowsC = [
|
||||||
|
{ lab: 'Arc Ramping Interval', sub: '분할 간격', cells: [
|
||||||
|
{ t: '0 mm', tag: ['ng', 'NG'], ng: true, sub: 'theta=0 · 카운터 붕괴',
|
||||||
|
body: JUMP(poly([ORIG, START])) + BAD(poly([END, ORIG])) + MARK(poly(arcBase)) + xmark(ORIG) + note(10, 24, '✕ 0으로 나눗셈', 'var(--st-crit)'),
|
||||||
|
tip: [['값', '0 mm (기본값)'], ['theta', '0°'], ['결과', '(int)(Dist/0) = int.MinValue → 21.5억 회 루프'], ['조치', '0.1 mm로 설정']] },
|
||||||
|
{ t: '0.1 mm', tag: ['ok', '권장'], sub: '157분할 · 매끄러움',
|
||||||
|
body: JUMP(poly([ORIG, START])) + rampingBody(2 / 15.71, 2 / 15.71, 10),
|
||||||
|
tip: [['값', '0.1 mm (툴팁 권장값)'], ['theta', '0.573°'], ['분할', '157'], ['결과', '파워 계단이 육안으로 안 보임']] },
|
||||||
|
{ t: '2.0 mm', tag: ['wr', '거침'], sub: '8분할 · 계단 발생',
|
||||||
|
body: JUMP(poly([ORIG, START])) + rampingBody(0.25, 0.25, 4),
|
||||||
|
tip: [['값', '2.0 mm'], ['theta', '11.46°'], ['분할', '8'], ['결과', '파워 계단이 비드에 보일 수 있음']] },
|
||||||
|
]},
|
||||||
|
{ lab: 'Ramping 거리 합', sub: 'Start + End', cells: [
|
||||||
|
{ t: '4 mm', tag: ['ok', '권장'], sub: '호길이 15.71의 25%',
|
||||||
|
body: JUMP(poly([ORIG, START])) + rampingBody(2 / 15.71, 2 / 15.71, 10),
|
||||||
|
tip: [['Start+End', '4 mm'], ['호길이', '15.71 mm'], ['비율', '25%'], ['결과', 'rising20 + const117 + falling20 = 157 ✓']] },
|
||||||
|
{ t: '8 mm', tag: ['ok', '상한'], sub: '호길이의 50%',
|
||||||
|
body: JUMP(poly([ORIG, START])) + rampingBody(0.25, 0.25, 10),
|
||||||
|
tip: [['Start+End', '8 mm'], ['비율', '51%'], ['결과', '정출력 구간이 거의 사라짐 — 실용 상한']] },
|
||||||
|
{ t: '20 mm', tag: ['ng', 'NG'], ng: true, sub: '호길이 초과 → 과회전',
|
||||||
|
body: JUMP(poly([ORIG, START])) + rampingBody(0.42, 0.42, 8) + BAD(poly(arcPts(CX, CY, R, A1, A1 + 24.6))) + xmark(arcPts(CX, CY, R, A1 + 24.6, A1 + 24.6)[0]) + note(10, 24, '✕ 24.6° 초과 회전', 'var(--st-crit)'),
|
||||||
|
tip: [['Start+End', '20 mm'], ['호길이', '15.71 mm — 초과'], ['가드', 'R×|Sweep°|=900 과 비교해서 통과해버림'], ['결과', '114.6° 회전 + _Last 불일치']] },
|
||||||
|
]},
|
||||||
|
{ lab: 'LaserOnDelay', sub: '음수 = 선행 발광', cells: [
|
||||||
|
{ t: '−1800 µs', tag: ['wr', '현장값'], sub: '점프 끝 1.8 mm 발광',
|
||||||
|
body: (() => { const p = [ORIG, START]; const pre = [[START[0] - (START[0] - ORIG[0]) * 0.18, START[1] - (START[1] - ORIG[1]) * 0.18], START];
|
||||||
|
return JUMP(poly(p)) + BAD(poly(pre)) + MARK(poly(arcBase)) + note(10, 24, '✕ 점프 구간 선행 발광', 'var(--st-crit)'); })(),
|
||||||
|
tip: [['값', '−1800 µs'], ['JumpSpeed', '1000 mm/s'], ['선행 발광', '1.8 mm'], ['비고', 'JumpDelay=0이면 미러 이동 중 발광']] },
|
||||||
|
{ t: '−200 µs', tag: ['ok', '권장'], sub: '0.2 mm — 시작점 보정용',
|
||||||
|
body: (() => { const pre = [[START[0] - (START[0] - ORIG[0]) * 0.03, START[1] - (START[1] - ORIG[1]) * 0.03], START];
|
||||||
|
return JUMP(poly([ORIG, START])) + BAD(poly(pre)) + MARK(poly(arcBase)); })(),
|
||||||
|
tip: [['값', '−200 µs'], ['선행 발광', '0.2 mm'], ['용도', '시작부 언더필 방지']] },
|
||||||
|
{ t: '0 µs', tag: ['ok', '기본'], sub: '선행 발광 없음',
|
||||||
|
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)),
|
||||||
|
tip: [['값', '0 µs'], ['선행 발광', '없음'], ['비고', '시작부 출력 상승 지연이 보이면 음수로 조정']] },
|
||||||
|
]},
|
||||||
|
{ lab: 'Wobble 진폭', sub: 'Transversal', cells: [
|
||||||
|
{ t: '0.3 mm', tag: ['ok', '좁음'], sub: '비드폭 ≈ 스팟 + 0.6',
|
||||||
|
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 3, 1.5, 16, 0))),
|
||||||
|
tip: [['Transversal', '0.3 mm'], ['Longitudinal', '0.15 mm'], ['Shape', 'ELLIPSE'], ['용도', '갭이 작을 때']] },
|
||||||
|
{ t: '1.0 mm', tag: ['ok', '표준'], sub: '갭 브리징 표준',
|
||||||
|
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 7, 3.5, 12, 0))),
|
||||||
|
tip: [['Transversal', '1.0 mm'], ['Longitudinal', '0.5 mm'], ['Shape', 'ELLIPSE']] },
|
||||||
|
{ t: '3.0 mm', tag: ['wr', '주의'], sub: '입열 분산 · 관통 저하',
|
||||||
|
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 14, 7, 8, 0))),
|
||||||
|
tip: [['Transversal', '3.0 mm'], ['주의', '진폭이 커질수록 단위면적 입열 감소'], ['동반 조정', 'MarkSpeed↓ 또는 Power↑']] },
|
||||||
|
]},
|
||||||
|
{ lab: 'Wobble 주파수', sub: 'vs MarkSpeed', cells: [
|
||||||
|
{ t: '50 Hz', tag: ['wr', '성김'], sub: '0.5 loop/mm — 흔적 보임',
|
||||||
|
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 7, 3.5, 4, 0))),
|
||||||
|
tip: [['Frequency', '50 Hz'], ['MarkSpeed', '100 mm/s'], ['밀도', '0.5 loop/mm'], ['결과', '와블 궤적이 비드에 그대로 남음']] },
|
||||||
|
{ t: '150 Hz', tag: ['ok', '권장'], sub: '1.5 loop/mm',
|
||||||
|
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 7, 3.5, 12, 0))),
|
||||||
|
tip: [['Frequency', '150 Hz'], ['MarkSpeed', '100 mm/s'], ['밀도', '1.5 loop/mm'], ['결과', '균일한 비드']] },
|
||||||
|
{ t: '300 Hz', tag: ['ok', '조밀'], sub: '3.0 loop/mm — 스캐너 한계',
|
||||||
|
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + WOB(poly(wobbleArcPts(CX, CY, R, A0, A1, 7, 3.5, 24, 0))),
|
||||||
|
tip: [['Frequency', '300 Hz (최대)'], ['밀도', '3.0 loop/mm'], ['주의', '고주파에서 실제 진폭이 설정보다 작아질 수 있음']] },
|
||||||
|
]},
|
||||||
|
{ lab: 'PolygonDelay', sub: 'Arc 분절 간 정정', cells: [
|
||||||
|
{ t: '0 µs', tag: ['wr', '현장값'], sub: '정정 대기 없음 · 오버슈트',
|
||||||
|
body: (() => { const p = arcPts(CX, CY, R, A0, A1, 12).map((q, i) => [q[0] + (i % 2 ? 2.2 : -2.2), q[1] + (i % 2 ? -1.6 : 1.6)]);
|
||||||
|
return JUMP(poly([ORIG, START])) + MARK(poly(p)) + note(10, 24, '분절 경계에서 궤적 흔들림', 'var(--st-serious)'); })(),
|
||||||
|
tip: [['값', '0 µs'], ['영향', 'Arc 분절 사이에 정정 대기 없음'], ['증상', '원호 오버슈트 / 각짐']] },
|
||||||
|
{ t: '50 µs', tag: ['ok', '권장'], sub: '균형',
|
||||||
|
body: (() => { const p = arcPts(CX, CY, R, A0, A1, 12).map((q, i) => [q[0] + (i % 2 ? 0.6 : -0.6), q[1]]);
|
||||||
|
return JUMP(poly([ORIG, START])) + MARK(poly(p)); })(),
|
||||||
|
tip: [['값', '50~100 µs'], ['효과', '분절 경계 정정'], ['비용', '분절 수 × 50 µs 만큼 T/T 증가']] },
|
||||||
|
{ t: '500 µs', tag: ['wr', '과대'], sub: 'T/T 급증 · 입열 집중',
|
||||||
|
body: JUMP(poly([ORIG, START])) + MARK(poly(arcBase)) + s('g', {}, arcPts(CX, CY, R, A0, A1, 12).map(q => dot(q, 'var(--st-serious)')).join('')) + note(10, 24, '분절마다 정지 → 입열 집중', 'var(--st-serious)'),
|
||||||
|
tip: [['값', '500 µs'], ['분절 157개 기준', 'T/T +78 ms'], ['위험', '정지 지점마다 과입열 → 언더컷/버닝']] },
|
||||||
|
]},
|
||||||
|
];
|
||||||
|
|
||||||
|
document.getElementById('gridC').innerHTML = rowsC.map(r => `
|
||||||
|
<div class="rowgrid" style="grid-template-columns:132px repeat(3,minmax(0,1fr)); margin-bottom:12px">
|
||||||
|
<div class="rowlab">${r.lab}<span>${r.sub}</span></div>
|
||||||
|
${r.cells.map(c => cell({ title: c.t, sub: c.sub, tag: c.tag, ng: c.ng, body: c.body,
|
||||||
|
tip: TIP(`${r.lab} = ${c.t}`, c.tip) })).join('')}
|
||||||
|
</div>`).join('');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
Doc/인수인계/260907_Arc출사_파라미터_시각화.pptx
Normal file
BIN
Doc/인수인계/260907_Arc출사_파라미터_시각화.pptx
Normal file
Binary file not shown.
BIN
Doc/인수인계/260907_Draw타입별_설정가이드_현장용.pptx
Normal file
BIN
Doc/인수인계/260907_Draw타입별_설정가이드_현장용.pptx
Normal file
Binary file not shown.
191
Doc/인수인계/260907_LaserFrequency_단위계통_분석.md
Normal file
191
Doc/인수인계/260907_LaserFrequency_단위계통_분석.md
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
# Laser Frequency / Pulse Duration 단위 계통 분석
|
||||||
|
|
||||||
|
- 작성 : 2026-09-07
|
||||||
|
- 계기 : 현장 Arc 출사 이상 → 조치 후 정상. 실제 원인은 **Frequency 값 오입력**으로 확인됨.
|
||||||
|
그 값이 어떤 경로로 스캐너에 들어가는지 역추적한 결과.
|
||||||
|
- 범위 : **정적 분석 전용. 소스 미수정. 현장 검증 미실시.**
|
||||||
|
- 결론 요약 : 화면값 → RTC 전달 경로에 **단위/캐스팅 문제 3건 + 적용 범위 문제 1건 + 크래시 경로 1건**이 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 값이 흘러가는 경로
|
||||||
|
|
||||||
|
```
|
||||||
|
Scanner Parameter 화면
|
||||||
|
Laser Frequency(kHz) double, 범위 0.2 ~ 5 ScannerParam.cs:142
|
||||||
|
Laser Pulse Duration(ms) double ScannerParam.cs:164
|
||||||
|
│
|
||||||
|
▼ SeqUploadWeldingList.InitializeDeviceStatus() :206-212
|
||||||
|
IDrawObject firstObject = _InitParam.SelectedDrawObjectList[0] as IDrawObject;
|
||||||
|
int firstObjectScannerNo = firstObject.ScannerParamID - 1;
|
||||||
|
_ScannerParam = ScannerCanvasUserCtrl.ScannerParams[firstObjectScannerNo];
|
||||||
|
|
||||||
|
_Scanner.SetLaserTiming( (uint)_ScannerParam.LaserFrequency,
|
||||||
|
_ScannerParam.LaserPulseDuration,
|
||||||
|
_ScannerParam.LaserPulseDuration );
|
||||||
|
│
|
||||||
|
▼ Scanner.cs:163
|
||||||
|
SetLaserTiming(uint periodHz, double laser1PulseWidthUs, double laser2PulseWidthUs)
|
||||||
|
│
|
||||||
|
▼ ScannerRTC6.cs:772-784 (ScannerRTC5.cs:770-782 동일)
|
||||||
|
//uint halfPeriodBit = (32 / periodkHz) * 1000; ← 구버전(주석 처리됨)
|
||||||
|
uint halfPeriodBit = (uint)((32.0 / (double)periodHz) * 1000000.0);
|
||||||
|
uint laser1PulseWidthBit = (uint)(laser1PulseWidthUs * 64);
|
||||||
|
uint laser2PulseWidthBit = (uint)(laser2PulseWidthUs * 64);
|
||||||
|
RTC6Wrap.n_set_laser_timing(dev, halfPeriodBit, laser1PulseWidthBit, laser2PulseWidthBit, 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
기준 단위 : RTC의 half period·pulse width는 **1/64 µs (15.625 ns)** 단위.
|
||||||
|
펄스폭 변환이 `× 64` 인 것으로 이 기준이 확인됩니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 발견 사항
|
||||||
|
|
||||||
|
### ① `(uint)` 캐스팅이 소수점을 버립니다 — `SeqUploadWeldingList.cs:211`
|
||||||
|
|
||||||
|
`LaserFrequency` 는 **double** 이고 화면 설명상 범위가 **0.2 ~ 5 kHz** 입니다.
|
||||||
|
그런데 전달 시 `(uint)` 로 캐스팅합니다.
|
||||||
|
|
||||||
|
| 화면 입력 | `(uint)` 후 | 비고 |
|
||||||
|
|---|---|---|
|
||||||
|
| 0.2 | **0** | 0 이 되어 아래 ②에서 0 나눗셈 |
|
||||||
|
| 0.5 | **0** | 〃 |
|
||||||
|
| 2.5 | 2 | 0.5 손실 |
|
||||||
|
| 4.9 | 4 | 0.9 손실 |
|
||||||
|
| 5 | 5 | 정상 |
|
||||||
|
|
||||||
|
**화면에서는 소수 입력이 가능한데 전달 과정에서 정수로 잘립니다.**
|
||||||
|
설명상 범위(0.2~5)의 상당 부분이 0 또는 절삭값이 됩니다.
|
||||||
|
|
||||||
|
### ② 0 이 들어가면 0 으로 나눕니다 — `ScannerRTC6.cs:775`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
uint halfPeriodBit = (uint)((32.0 / (double)periodHz) * 1000000.0);
|
||||||
|
```
|
||||||
|
|
||||||
|
`periodHz == 0` 이면 `32.0 / 0` = `+Infinity` → `(uint)+Infinity` 는 **C# 명세상 정의되지 않은 동작**입니다.
|
||||||
|
(x64 실측 동작은 0) 예외는 발생하지 않고 **조용히 엉뚱한 값**이 RTC로 넘어갑니다.
|
||||||
|
|
||||||
|
### ③ 환산식이 1000배 큽니다 — `ScannerRTC6.cs:775` / `ScannerRTC5.cs:773`
|
||||||
|
|
||||||
|
정확한 환산 (f 단위 kHz, 결과 단위 1/64 µs) :
|
||||||
|
|
||||||
|
```
|
||||||
|
반주기[µs] = 1 / (2 · f[kHz] · 1000) · 1e6 = 500 / f
|
||||||
|
halfPeriodBit = 반주기[µs] × 64 = 32000 / f
|
||||||
|
```
|
||||||
|
|
||||||
|
| 구분 | 식 | f=5 일 때 |
|
||||||
|
|---|---|---|
|
||||||
|
| 정확값 | `32000 / f` | 6,400 |
|
||||||
|
| **현재 코드** | `(32.0 / f) × 1,000,000` | **6,400,000** |
|
||||||
|
| 바로 위 주석(구버전) | `(32 / f) × 1000` | 6,000 ※정수나눗셈 절삭은 있으나 자릿수는 맞음 |
|
||||||
|
|
||||||
|
**현재 코드는 정확값의 1000배**입니다. 주석 처리된 구버전이 자릿수상 맞는 식이었습니다.
|
||||||
|
|
||||||
|
결과적으로 **화면에 kHz 로 넣은 값이 실제로는 Hz 수준으로 동작**합니다.
|
||||||
|
|
||||||
|
| 화면 입력 | 의도 | 실제 (현재 코드) |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 kHz | 1,000 Hz | 1 Hz |
|
||||||
|
| 2.5 kHz | 2,500 Hz | 2 Hz (①의 절삭까지 겹침) |
|
||||||
|
| 5 kHz | 5,000 Hz | 5 Hz |
|
||||||
|
|
||||||
|
### ④ Pulse Duration 이 ms → µs 변환 없이 전달됩니다 — `SeqUploadWeldingList.cs:211`
|
||||||
|
|
||||||
|
- 화면 항목 : `Laser Pulse Duration(ms)` — **ms**
|
||||||
|
- 받는 파라미터 : `laser1PulseWidthUs` — **µs**
|
||||||
|
- 활성 코드 : `_ScannerParam.LaserPulseDuration` **그대로 전달** (변환 없음)
|
||||||
|
- 같은 파일에 주석 처리된 코드(`:628`) : `(uint)(param.LaserPulseDuration * 1000)` ← **ms→µs 변환이 있었음**
|
||||||
|
|
||||||
|
| 화면 입력 | 의도 (ms→µs) | 실제 |
|
||||||
|
|---|---|---|
|
||||||
|
| 0.1 ms | 100 µs → 6,400 bit | 0.1 → **6 bit ≈ 0.094 µs** |
|
||||||
|
| 1.0 ms | 1,000 µs → 64,000 bit | 1.0 → **64 bit = 1 µs** |
|
||||||
|
|
||||||
|
**1000배 작게** 전달됩니다.
|
||||||
|
|
||||||
|
### ⑤ 첫 번째 객체의 값만 적용됩니다 — `SeqUploadWeldingList.cs:206-212`
|
||||||
|
|
||||||
|
`SetLaserTiming` 은 **시퀀스당 1회**, `SelectedDrawObjectList[0]` 의 Parameter ID 값으로만 호출됩니다.
|
||||||
|
리스트가 아니라 COMMAND 방식이라 리스트 실행 전에 한 번 세팅되고 끝입니다.
|
||||||
|
|
||||||
|
→ **Parameter ID 2, 3 … 의 `Laser Frequency` · `Laser Pulse Duration` 은 무시됩니다.**
|
||||||
|
(`Laser Power`, `Mark Speed`, `Delay` 등은 객체마다 리스트에 들어가므로 정상 적용됩니다)
|
||||||
|
|
||||||
|
현장에서 "ID 마다 Frequency 를 다르게 넣었는데 차이가 없다" 면 이것이 원인입니다.
|
||||||
|
|
||||||
|
### ⑥ 크래시 경로 — `SeqUploadWeldingList.cs:207-209`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
int firstObjectScannerNo = firstObject.ScannerParamID - 1;
|
||||||
|
_ScannerParam = ScannerCanvasUserCtrl.ScannerParams[firstObjectScannerNo];
|
||||||
|
```
|
||||||
|
|
||||||
|
`ScannerParamID` 가 0 이면 인덱스 **-1** → `IndexOutOfRangeException`.
|
||||||
|
`SelectedDrawObjectList` 가 비어 있으면 `[0]` 에서 예외.
|
||||||
|
둘 다 방어 코드가 없습니다. (2026-09-07 추가한 예외처리는 `FuncLoadDrawObjectScannerParamSelected` 쪽이라 이 경로는 여전히 무방비)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 정황 근거
|
||||||
|
|
||||||
|
`SeqUploadWeldingList.cs:624-626` 주석 :
|
||||||
|
|
||||||
|
> TrumpLaser의 경우 SetLaserTiming 함수로 Gate 신호를 살려줘야 함
|
||||||
|
> 우선 무조건 셋팅하는 것으로 쓰고 …
|
||||||
|
> **LaserFrequency = 10, LaserPulseDuration = 0.1로 셋팅하면 출사 되는 것 확인_241025**
|
||||||
|
|
||||||
|
물리적으로 계산해서 정한 값이 아니라 **"이 값을 넣으니 나오더라"로 찾은 경험값**이라는 기록입니다.
|
||||||
|
위 ①~④의 단위 오류가 있는 상태에서 경험적으로 맞춘 것이므로 서로 정합합니다.
|
||||||
|
|
||||||
|
이 값으로 현재 코드가 만드는 실제 신호 :
|
||||||
|
- halfPeriodBit = 32,000,000 / 10 = 3,200,000 bit = 50,000 µs → **10 Hz**
|
||||||
|
- pulseWidthBit = 0.1 × 64 = 6 bit → **약 0.094 µs**
|
||||||
|
|
||||||
|
즉 **10 Hz 주기에 0.1 µs 폭**의 신호입니다. 듀티로 보면 거의 0 에 가깝습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 그래서 실제로 문제인가 — 판단 보류
|
||||||
|
|
||||||
|
**단위 오류 자체는 코드상 명확합니다.** 다만 실제 용접 품질에 영향을 주는지는 별개입니다.
|
||||||
|
|
||||||
|
현재 본 레이저는 **아날로그 파워(DA) + 게이트(DO Laser_REMOTE)** 로 제어됩니다
|
||||||
|
(`SeqAutoRunPTOP.cs:1305`, `SetScannerProcessCondition` 의 `WriteDA`).
|
||||||
|
이 구조에서 RTC 의 LASER1/LASER2 펄스 신호는 **"살아만 있으면 되는" 역할**일 수 있습니다.
|
||||||
|
위 주석도 "Gate 신호를 살려줘야 함" 이라고 적고 있습니다.
|
||||||
|
|
||||||
|
→ **발진기가 이 펄스 신호를 실제로 어떻게 쓰는지 확인해야 결론이 납니다.**
|
||||||
|
발진기 매뉴얼의 변조/게이트 입력 사양 확인 필요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 확인·조치 후보 (미실행)
|
||||||
|
|
||||||
|
| # | 항목 | 내용 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 발진기 사양 확인 | RTC LASER1/LASER2 펄스를 실제 변조에 쓰는지, 게이트로만 쓰는지 |
|
||||||
|
| 2 | 현재 현장값 확인 | 각 레시피 Parameter ID 의 `Laser Frequency` · `Laser Pulse Duration` 실측 |
|
||||||
|
| 3 | ①⑤ 우선 | 소수점 절삭과 "첫 객체만 적용"은 사양 확인 없이도 **현장이 오해하기 쉬운 지점** |
|
||||||
|
| 4 | ③④ 수정 시 | 환산식을 고치면 **현재 경험값이 전부 어긋납니다.** 반드시 재티칭 전제로 진행 |
|
||||||
|
| 5 | ⑥ 방어 | `ScannerParamID <= 0` / 빈 리스트 방어 추가 |
|
||||||
|
|
||||||
|
**주의 — ③④ 를 고치면 현재 잘 나오는 조건이 깨집니다.** 경험값이 오류를 상쇄하고 있을 수 있으므로,
|
||||||
|
발진기 사양 확인 → 시험 조건 확보 → 재티칭 계획이 선 뒤에 손대야 합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 참조 위치
|
||||||
|
|
||||||
|
| 위치 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| `ScannerParam.cs:142` | `LaserFrequency` double, kHz, 0.2~5 |
|
||||||
|
| `ScannerParam.cs:164` | `LaserPulseDuration` double, ms |
|
||||||
|
| `SeqUploadWeldingList.cs:206-212` | 첫 객체 param 으로 `SetLaserTiming` 1회 호출 + `(uint)` 캐스팅 |
|
||||||
|
| `SeqUploadWeldingList.cs:624-629` | 경험값 주석 + ms→µs 변환이 있던 주석 코드 |
|
||||||
|
| `Scanner.cs:163-169` | 래퍼 (`periodHz`, `laser1PulseWidthUs`) |
|
||||||
|
| `ScannerRTC6.cs:772-784` | 환산식 — 1000배 문제 지점 |
|
||||||
|
| `ScannerRTC5.cs:770-782` | RTC5 동일 |
|
||||||
|
| `ScannerObj.cs:264` | 추상 선언 (`halfPeriod`, `laser1PulseWidth`) — 이름이 계층마다 다름 |
|
||||||
158
Doc/인수인계/260907_NRE_크래시_위험구간_점검.md
Normal file
158
Doc/인수인계/260907_NRE_크래시_위험구간_점검.md
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
# NRE / 크래시 위험 구간 점검
|
||||||
|
|
||||||
|
- 작성 : 2026-09-07
|
||||||
|
- 계기 : 출사 미지원 객체(Ellipse/Rectangle/Roi) NRE 방어(`ISSUE-260907-01`) 적용 후,
|
||||||
|
같은 유형이 더 있는지 전수 확인 요청.
|
||||||
|
- 성격 : **점검 목록 전용. 소스 미수정. 현장 검증 미실시.**
|
||||||
|
- 표기 : 🟩 소스 확인 / 🟦 현장 기록 / 🟨 미확보·판정 불가
|
||||||
|
|
||||||
|
## 점검 범위와 한계
|
||||||
|
|
||||||
|
| 구분 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 포함 | `Machines/ILSOS_PrismaticRD_PTOP/**`, `WeldingComponents/WeldingCommon/Sequences/**` |
|
||||||
|
| 제외 | AuroraVision, LASA GUI(Canvas·DrawTools), AuroraDevices 하위 대부분, Aurora 프레임워크 |
|
||||||
|
| 방법 | 패턴 검색(`FirstOrDefault`/`First()`/`[0]`/`as`/인덱서) 후 해당 라인 전후 확인 |
|
||||||
|
| 한계 | 정적 분석. 런타임 재현·디버깅 없음. 제외 범위에 같은 유형이 더 있을 수 있음 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 선행 확인 — 예외가 실제로 프로그램을 종료시키는가
|
||||||
|
|
||||||
|
🟩 우리 소스에 **전역 예외 핸들러가 없습니다.**
|
||||||
|
`DispatcherUnhandledException` / `AppDomain.CurrentDomain.UnhandledException` /
|
||||||
|
`UnobservedTaskException` / `SetUnhandledExceptionMode` 검색 결과 **0건**.
|
||||||
|
|
||||||
|
🟩 `ILSOS_PrismaticRD_PTOP.csproj` → `OutputType=library`.
|
||||||
|
즉 실행 주체는 **Aurora.exe** 이고 저장소에 소스가 없습니다.
|
||||||
|
|
||||||
|
🟨 따라서 아래 항목이 **실제로 프로세스를 종료시키는지는 판정할 수 없습니다.**
|
||||||
|
- Aurora.exe 가 전역 핸들러를 걸어 두었는지 미확인
|
||||||
|
- Aurora `SequenceBuilder` 가 노드 실행을 try/catch 로 감싸는지 미확인
|
||||||
|
(`sb.OnForceError(...)` 훅이 존재하므로 **잡을 가능성은 있음**)
|
||||||
|
|
||||||
|
> **이 문서는 "NRE 가 발생할 수 있는 지점" 목록입니다.**
|
||||||
|
> "여기서 프로그램이 죽는다" 로 읽지 마십시오. 종료 여부는 별도 확인 필요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A. 널 체크 없이 즉시 역참조 — 위험도 최상
|
||||||
|
|
||||||
|
### A-1. `SeqComponentTeachingRun.cs:456-459` 🟩
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var power = Status.GetProcessingWeldingData().UploadProcessDatas
|
||||||
|
.Where(data => data.DataName == $"Power_{SlotNo}_{_SequenceSetting.LastPageNo}")
|
||||||
|
.FirstOrDefault();
|
||||||
|
power.Value = ScannerCanvasUserCtrl.ScannerParams[paramIndex].LaserPower1
|
||||||
|
+ ScannerCanvasUserCtrl.ScannerParams[paramIndex].LaserPower2; // ← power == null 이면 NRE
|
||||||
|
|
||||||
|
var velocity = ...Where(data => data.DataName == $"Velocity_{SlotNo}_{...}").FirstOrDefault();
|
||||||
|
velocity.Value = ScannerCanvasUserCtrl.ScannerParams[paramIndex].MarkSpeed; // ← 동일
|
||||||
|
```
|
||||||
|
|
||||||
|
- `DataName` 을 `SlotNo` + `LastPageNo` 로 **문자열 조합**해 찾습니다.
|
||||||
|
- 그 조합이 `UploadProcessDatas` 에 없으면 `FirstOrDefault()` 가 `null` 을 반환합니다.
|
||||||
|
- **페이지 수가 늘거나 슬롯 번호 체계가 바뀌면 즉시 발생**합니다.
|
||||||
|
- 실제 운전 경로(Manual 티칭런)라 재현 가능성이 목록 중 가장 높습니다.
|
||||||
|
|
||||||
|
### A-2. `SeqEventQTest.cs:142-145` 🟩 — 3중 위험
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
FovInfo _fovInfo = VisionRecipeManager.GetFovInfos().FirstOrDefault();
|
||||||
|
_PatternAgent = FovPatternAgentManager.GetAgent(_fovInfo.ToSubSysInfo()); // ① _fovInfo null → NRE
|
||||||
|
List<PatternInfo> patterns = _PatternAgent.GetMainPatterns(_fovInfo); // ② agent null → NRE
|
||||||
|
_PatternInfo = patterns[0]; // ③ 빈 리스트 → IndexOutOfRange
|
||||||
|
```
|
||||||
|
|
||||||
|
### A-3. `SeqAutoRunPrismatic.cs:1101-1104` 🟩
|
||||||
|
|
||||||
|
A-2 와 동일 패턴 + `_Patterns[_InitParam.VisionRecipeNo]` 인덱스 무검증.
|
||||||
|
|
||||||
|
> 🟩 **참고 — 올바른 패턴이 같은 파일에 이미 있습니다.**
|
||||||
|
> `SeqAutoRunPrismatic.cs:743-745` 는 `_PatternResult?.Results?.FirstOrDefault()` 로
|
||||||
|
> 널 조건 연산자를 쓰고 이후 `if (baseResult != null)` 까지 확인합니다.
|
||||||
|
> A-2 · A-3 만 이 방어가 빠져 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B. `as` 캐스팅 결과를 가드 없이 보관 — 지연 발화형
|
||||||
|
|
||||||
|
캐스팅 실패 또는 키 부재 시 조용히 `null` 이 되고, **한참 뒤 그 필드를 사용하는 시점**에 터집니다.
|
||||||
|
발생 위치와 원인 위치가 달라 추적이 어렵습니다.
|
||||||
|
|
||||||
|
| 위치 | 대상 | 🟩 |
|
||||||
|
|---|---|---|
|
||||||
|
| `SeqAutoRunPTOP.cs:291` | `_SystemParameter` | 🟩 |
|
||||||
|
| `Views/Main/UCMainAutoRunViewModel.cs:95-97` | `_SystemParameter`, `_Scanner`, `_Plc` | 🟩 |
|
||||||
|
| `Views/Main/UCMainAutoRunViewModel.cs:472, 505` | `_SystemParameter` 재대입 | 🟩 |
|
||||||
|
| `SeqTestRun.cs:472` | `_IOLaser` | 🟩 |
|
||||||
|
| `SeqComponentTeachingRun.cs:179` | `_SequenceSetting` | 🟩 |
|
||||||
|
| `Monitoring/SeqStatusMonitor.cs:60` | `_SequenceSetting` | 🟩 |
|
||||||
|
| `SeqAutoRunPrismatic.cs:1050` | `SystemSetting` | 🟩 |
|
||||||
|
| `EquipmentSetup.cs:90, 199, 240, 342` | `EquipmentInfo` | 🟩 |
|
||||||
|
| `EquipmentSetup.cs:129, 133` | `IODevice`, `PLCControllerPTOP` | 🟩 |
|
||||||
|
| `Recipes/RMSHandlerForPTOP.cs:52-53` | `_plc`, `_RmsSetting` | 🟩 |
|
||||||
|
|
||||||
|
대부분 **초기화 구간**이라 정상 구성에서는 성공합니다.
|
||||||
|
위험은 "설정 키 이름 변경 / 디바이스 등록 누락 / 타입 교체" 시 **즉시 실패하지 않는다**는 점입니다.
|
||||||
|
|
||||||
|
🟩 **예외 — 이미 방어된 곳** : `SeqAutoRunPTOP.cs:1140, 1252` 의 `LaserInterlockSetting` 은
|
||||||
|
`LaserAngleOffsetSelector.Select()` 안에서 `setting != null` 삼항 처리로 방어됩니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## C. 인덱스 무검증 — NRE 는 아니나 동일하게 종료 요인
|
||||||
|
|
||||||
|
| 위치 | 위험 | 🟩 |
|
||||||
|
|---|---|---|
|
||||||
|
| `SeqUploadWeldingList.cs:206` | `SelectedDrawObjectList[0]` — 빈 리스트 | 🟩 |
|
||||||
|
| `SeqUploadWeldingList.cs:207-209` | `ScannerParams[ScannerParamID - 1]` — **ID 가 0 이면 인덱스 -1** | 🟩 |
|
||||||
|
| `SeqUploadWeldingList.cs:474, 617-631, 719, 1006, 1052, 1152, 1204, 1404, 1605` | `ScannerParams[obj.ScannerParamNo]` 범위 무검증 (배열은 128개 고정 생성) | 🟩 |
|
||||||
|
| `SeqWaitWeldOrChangePage.cs:194-195` | `ScannerParams[paramIndex]` | 🟩 |
|
||||||
|
| `SeqTestRun.cs:237-238` | `ScannerParams[paramIndex]` | 🟩 |
|
||||||
|
| `SeqComponentTeachingRun.cs:457-465` | `ScannerParams[paramIndex]` | 🟩 |
|
||||||
|
| `SeqAutoRunPrismatic.cs:1104` | `_Patterns[VisionRecipeNo]` | 🟩 |
|
||||||
|
|
||||||
|
**`SeqUploadWeldingList.cs:206-209` 주의** — 2026-09-07 적용한 예외처리는
|
||||||
|
`FuncLoadDrawObjectScannerParamSelected()` 쪽이라 **이 경로는 여전히 무방비**입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D. 예외는 안 나지만 값이 오염되는 것 (참고)
|
||||||
|
|
||||||
|
double 나눗셈이라 예외 대신 `Infinity`/`NaN` 이 조용히 흘러갑니다.
|
||||||
|
|
||||||
|
| 위치 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| `ScannerRTC6.cs:775` · `ScannerRTC5.cs:773` | `32.0 / periodHz` — `periodHz == 0` 이면 `Infinity` → `(uint)` 캐스팅은 정의되지 않은 동작 |
|
||||||
|
| `SeqUploadWeldingList.cs:1313-1316` | `ArcRampingInterval == 0` 시 동일 |
|
||||||
|
|
||||||
|
상세 : [`260907_LaserFrequency_단위계통_분석.md`](260907_LaserFrequency_단위계통_분석.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 우선순위 제안
|
||||||
|
|
||||||
|
| 순위 | 항목 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | **A-1** | 실제 운전 경로(Manual 티칭런) + 조합 문자열 탐색이라 재현 가능성 최상 |
|
||||||
|
| 2 | **C 의 `SeqUploadWeldingList.cs:206-209`** | 이번 예외처리가 커버하지 못한 인접 경로 |
|
||||||
|
| 3 | **A-2 / A-3** | 비전 경로. Vision 미사용이면 후순위 |
|
||||||
|
| 4 | **B** | 개별 수정보다 초기화 구간 일괄 널 가드 + 알람이 적절 |
|
||||||
|
| — | **선행** | Aurora.exe / SequenceBuilder 의 예외 처리 정책 확인. 이게 정해져야 "가드 후 어떻게 할지"(스킵 / 알람 / 시퀀스 중단)가 결정됨 |
|
||||||
|
|
||||||
|
## 조치 시 주의
|
||||||
|
|
||||||
|
- **조용히 스킵하면 안 되는 경우가 있습니다.** A-1 은 공정 데이터 상신 실패를 뜻하므로,
|
||||||
|
널이면 로그만 남기고 넘어가면 **데이터 누락이 은폐**됩니다. 알람 여부를 함께 정해야 합니다.
|
||||||
|
- 반대로 이번 `ISSUE-260907-01` 처럼 **원래도 동작하지 않던 경로**는 스킵이 맞습니다.
|
||||||
|
- 즉 "널이면 무조건 return" 을 일괄 적용하지 마십시오. 항목마다 판단이 다릅니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 변경 이력
|
||||||
|
|
||||||
|
| 일자 | 내용 | 비고 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-09-07 | 최초 작성. PTOP + WeldingCommon Sequences 범위 점검 | 소스 미수정 |
|
||||||
BIN
Doc/인수인계/260907_레시피화면_사용설명서_현장용.pptx
Normal file
BIN
Doc/인수인계/260907_레시피화면_사용설명서_현장용.pptx
Normal file
Binary file not shown.
BIN
Doc/인수인계/260915_레시피화면_작업매뉴얼_현장용.pptx
Normal file
BIN
Doc/인수인계/260915_레시피화면_작업매뉴얼_현장용.pptx
Normal file
Binary file not shown.
BIN
Doc/인수인계/260916_PTOP_프로그램_사용설명서.pptx
Normal file
BIN
Doc/인수인계/260916_PTOP_프로그램_사용설명서.pptx
Normal file
Binary file not shown.
BIN
Doc/인수인계/260916_자동운전_전환_사전점검_현장용.pptx
Normal file
BIN
Doc/인수인계/260916_자동운전_전환_사전점검_현장용.pptx
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user