Compare commits

...

2 Commits

Author SHA1 Message Date
to3796zw
8bbac47244 docs: 용어정리(가접 용접) 별도 파일 분리 + import
200줄 규칙 준수 위해 architecture.md §8 용어정리 본문을 docs/용어정리.md로 분리하고 @import로 연결.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 22:31:10 +09:00
to3796zw
041c15550c feat: 레시피별 Front/Down 레이저 인터락 추가
Use Interlock ON 시 ① 안전구역 게이트(기존) 유지 + ② 레시피별 방향 차단(신규). Recipe 폴더의 Recipe_Laser_Interlock.json(Front/Down 0/1)에 저장하고, 경로는 FieldHelper.json과 동일 소스(DB.Dao.SystemRecipe.WorkRecipeFile) 사용. Auto/Manual 양쪽 적용(Manual은 ①+② 신규), VM 체크박스는 레시피 전환 시 Background 우선순위 재로드로 stale 방지.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 22:30:57 +09:00
8 changed files with 208 additions and 14 deletions

View File

@ -15,6 +15,7 @@ using CoreUnit.Scanner.UserControl.ScannerCanvas;
using DB;
using GUI;
using Hermes;
using ILSOS_PrismaticRD_PTOP.Alarm;
using ILSOS_PrismaticRD_PTOP.Device;
using ILSOS_PrismaticRD_PTOP.Device.IO;
using ILSOS_PrismaticRD_PTOP.Settings;
@ -567,6 +568,50 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
Status.SetWorkingPosition(eWorkingPosition.EXECUTING_WELD);
Log.I($"Welding List Space : {_Scanner.GetListSpace()}");
// === Head Theta 인터락 (① 안전 게이트 + ② 레시피별 방향 차단) — Manual 경로 신규 추가 ===
var thetaSetting = context.GetSetting<LaserInterlockSetting>(SettingIds.LaserInterlock);
if (thetaSetting != null && thetaSetting.HeadThetaInterlockEnabled)
{
bool blocked = false;
if (_plc == null || !_plc.IsActivated)
{
Log.W("[DoComponentWelding] LaserInterlock - PLC 미연결/미활성 → 발진 차단(fail-closed)");
blocked = true;
}
else
{
short thetaRaw = _plc.ReadWord(eWordInput.HEAD_THETA_POSITION).data;
double scale = thetaSetting.HeadThetaPlcScale != 0 ? thetaSetting.HeadThetaPlcScale : 1; // 0 가드
double thetaDeg = thetaRaw / scale;
bool okFront = Math.Abs(thetaDeg - thetaSetting.HeadThetaFront) <= thetaSetting.HeadThetaTolerance;
bool okDown = Math.Abs(thetaDeg - thetaSetting.HeadThetaDown) <= thetaSetting.HeadThetaTolerance;
Log.I($"[DoComponentWelding] HeadTheta raw={thetaRaw}, deg={thetaDeg:F1} (Front={thetaSetting.HeadThetaFront}/Down={thetaSetting.HeadThetaDown}/±{thetaSetting.HeadThetaTolerance})");
// ① 안전 게이트: 안전 구역(Front/Down 부근) 아니면 차단
if (!okFront && !okDown)
{
Log.W($"[DoComponentWelding] ① HeadTheta 인터락 → Front/Down ±{thetaSetting.HeadThetaTolerance}° 벗어남(deg={thetaDeg:F1}) → 발진 차단");
blocked = true;
}
else
{
// ② 레시피별 방향 차단: 체크된 방향의 구역이면 차단
var rl = RecipeLaserInterlock.Load();
if ((rl.Front && okFront) || (rl.Down && okDown))
{
Log.W($"[DoComponentWelding] ② 레시피 차단 deg={thetaDeg:F1} Front?{rl.Front}/{okFront} Down?{rl.Down}/{okDown} → 발진 차단");
blocked = true;
}
}
}
if (blocked)
{
try { AlarmManager.Alarm(AlarmCode_PTOP.ALARM_LASER_HEAD_THETA_INTERLOCK); }
catch (Exception ex) { Log.E($"[DoComponentWelding] 알람 표시 실패: {ex.Message}"); }
return null;
}
}
DateTime weldingTime = DateTime.Now;
_Scanner.ExecuteList(_WeldingScannerListNo);
_Scanner.WaitForListTaskCompleted(1000);//(_SystemParameter.WeldLinesTaskTimeOut);

View File

@ -767,11 +767,22 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
bool okFront = Math.Abs(thetaDeg - thetaSetting.HeadThetaFront) <= thetaSetting.HeadThetaTolerance;
bool okDown = Math.Abs(thetaDeg - thetaSetting.HeadThetaDown) <= thetaSetting.HeadThetaTolerance;
Log.I($"[ExecuteWelding] HeadTheta raw={thetaRaw}, deg={thetaDeg:F1} (Front={thetaSetting.HeadThetaFront}/Down={thetaSetting.HeadThetaDown}/±{thetaSetting.HeadThetaTolerance})");
// ① 안전 게이트(기존): 안전 구역(Front/Down 부근) 아니면 차단
if (!okFront && !okDown)
{
Log.W($"[ExecuteWelding] HeadTheta 인터락 → Front/Down ±{thetaSetting.HeadThetaTolerance}° 벗어남(deg={thetaDeg:F1}) → 발진 차단");
Log.W($"[ExecuteWelding] ① HeadTheta 인터락 → Front/Down ±{thetaSetting.HeadThetaTolerance}° 벗어남(deg={thetaDeg:F1}) → 발진 차단");
blocked = true;
}
else
{
// ② 레시피별 방향 차단(신규): 체크된 방향의 구역이면 차단
var rl = WeldingCommon.Settings.RecipeLaserInterlock.Load();
if ((rl.Front && okFront) || (rl.Down && okDown))
{
Log.W($"[ExecuteWelding] ② 레시피 차단 deg={thetaDeg:F1} Front?{rl.Front}/{okFront} Down?{rl.Down}/{okDown} → 발진 차단");
blocked = true;
}
}
}
if (blocked)
{

View File

@ -0,0 +1,79 @@
using System;
using System.IO;
using DB;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace WeldingCommon.Settings
{
// 레시피별 Laser 방향(Front/Down) 발진 차단 체크값.
// 활성 레시피 폴더의 Recipe_Laser_Interlock.json 에 저장/로드한다.
// 경로 소스는 FieldHelper.json 과 동일한 DB.Dao.SystemRecipe.WorkRecipeFile 을 사용해
// 같은 레시피 폴더에 떨어지도록 보장한다.
public class RecipeLaserInterlock
{
public bool Front { get; set; } // json 1=true(차단) / 0=false
public bool Down { get; set; }
private const string FileName = "Recipe_Laser_Interlock.json";
private const string WrapperKey = "Laser Interlock";
// 활성 레시피 폴더의 Recipe_Laser_Interlock.json 전체 경로.
// (FieldHelper.json 과 동일 소스/폴더. 파일명만 치환)
public static string GetActiveFilePath()
{
string recipeFile = DB.Dao.SystemRecipe.WorkRecipeFile;
if (string.IsNullOrEmpty(recipeFile))
{
return null;
}
return recipeFile.Replace(Path.GetFileName(recipeFile), FileName);
}
// 파일이 없거나 파싱 실패 시 기본값(Front=false, Down=false) = 차단 없음.
public static RecipeLaserInterlock Load()
{
var result = new RecipeLaserInterlock();
try
{
string path = GetActiveFilePath();
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
return result;
}
var root = JObject.Parse(File.ReadAllText(path));
var node = root[WrapperKey];
if (node != null)
{
result.Front = (node["Front"]?.Value<int>() ?? 0) == 1;
result.Down = (node["Down"]?.Value<int>() ?? 0) == 1;
}
}
catch
{
// 손상/파싱 실패 → 기본값(차단 없음) 반환
}
return result;
}
public static void Save(RecipeLaserInterlock value)
{
string path = GetActiveFilePath();
if (string.IsNullOrEmpty(path))
{
return;
}
var root = new JObject
{
[WrapperKey] = new JObject
{
["Front"] = value.Front ? 1 : 0,
["Down"] = value.Down ? 1 : 0,
}
};
File.WriteAllText(path, root.ToString(Formatting.Indented));
}
}
}

View File

@ -98,6 +98,7 @@
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"></ColumnDefinition>
@ -158,6 +159,15 @@
<Button Grid.Column="1" Margin="3" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="auto" FontWeight="Bold" Style="{StaticResource StButtonAccentPurple}" Command="{Binding CMD_StopTeachingRun}" Content="Stop" IsEnabled="{Binding IsSequenceRunning_TeachingRun}"/>
</Grid>
<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>
</Grid>
</GroupBox>

View File

@ -82,6 +82,40 @@ namespace WeldingCommon.Views
private bool _IsObjectSelectAll;
public bool IsObjectSelectAll { get => _IsObjectSelectAll; set => SetProperty(ref _IsObjectSelectAll, value); }
// 레시피별 Laser 방향 발진 차단 체크 (Recipe_Laser_Interlock.json) — 변경 시 즉시 활성 레시피에 저장
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(); }
}
// 로드로 인한 set 시 저장 억제(레시피 전환 시 stale 덮어쓰기 방지)
private bool _suppressInterlockSave;
private void SaveLaserInterlock()
{
if (_suppressInterlockSave) {
return;
}
RecipeLaserInterlock.Save(new RecipeLaserInterlock { Front = _IsFrontInterlock, Down = _IsDownInterlock });
}
private void LoadLaserInterlock()
{
var rl = RecipeLaserInterlock.Load();
_suppressInterlockSave = true;
try {
IsFrontInterlock = rl.Front;
IsDownInterlock = rl.Down;
} finally {
_suppressInterlockSave = false;
}
}
// ScoutNormal 행 전환 sleep 분리 (같은 행 / 행 전환)
private int _ScoutSameRowSleepMs;
public int ScoutSameRowSleepMs
@ -135,6 +169,8 @@ namespace WeldingCommon.Views
InitVariable();
LoadLaserInterlock();
CMD_VisibleChanged = new DelegateCommand(VisibleChanged);
CMD_RunScoutNormal = new DelegateCommand(PerformCmdRunScoutNormal);
@ -154,6 +190,14 @@ namespace WeldingCommon.Views
.ObserveOnDispatcher()
.Subscribe(OnSettingReloaded);
// 레시피 전환 시 체크 상태를 새 레시피의 json 값으로 갱신.
// SYS_RECIPE_ACTIVATED(1-홉)에서 WorkRecipeFile은 아직 미갱신(ReloadRecipe는 2-홉 뒤) →
// Background 우선순위로 지연 실행해 ReloadRecipe(Normal) 완료 후 새 경로로 로드.
_Aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_RECIPE_ACTIVATED)
.ObserveOnDispatcher()
.Subscribe(_ => Application.Current.Dispatcher.BeginInvoke(
new Action(LoadLaserInterlock), DispatcherPriority.Background));
_MonitorTimer = new DispatcherTimer();
_MonitorTimer.Tick += OnMonitoring;
_MonitorTimer.Interval = TimeSpan.FromMilliseconds(100);
@ -182,6 +226,7 @@ namespace WeldingCommon.Views
{
bool visible = (bool)obj;
if (visible) {
LoadLaserInterlock(); // 패널 표시 시 현재 활성 레시피의 체크값 반영
_MonitorTimer.Start();
} else {
_MonitorTimer.Stop();

View File

@ -253,6 +253,7 @@
<DependentUpon>SequenceDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Settings\LaserPowerCalParam.cs" />
<Compile Include="Settings\RecipeLaserInterlock.cs" />
<Compile Include="Settings\SequenceSetting.cs" />
<Compile Include="Settings\SettingRmsDataList.cs" />
<Compile Include="Settings\SettingUploadDataList.cs" />

View File

@ -192,16 +192,4 @@ ILSOS_PrismaticRD_PTOP의 아키텍처·구현·빌드를 종합 분석하는
## 8. 용어 정리 (UNICELL PTOP WELDER)
> 협력사: **UBISAM** / 프로젝트: **UNICELL PTOP WELDING**(약칭 **PTOP**)
### 가접 용접 (Tack Welding)
- **정의**: U-Frame 본(메인) 용접 전에, **팩 제품과 프레임 간 밀착이 안 된 자재를 먼저 붙여 고정**하는 예비 용접.
- **이유**: 자재가 떠 있으면(밀착 불량) 본 용접 품질이 안 나오므로, **Align → 가접으로 밀착 → 메인 용접** 순서로 진행.
- **방식**: **Dot(점) 또는 Short Line(짧은 선)** 출사로 미리 몇 점 붙여 밀착.
- **운용**: 기존 AutoRun 시나리오엔 **가접 시나리오가 없었어서 신규 추가**됨. Manual 모드로 전환 후 Recipe 화면에서 **직접 Scout Image** 촬영 → 수동으로 가접(밀착) 용접 수행.
- **PLC 연동**: 이를 위해 PLC와 협의하여 **15번 bit(0x0F)** 추가 — `M70515 PLC_WELD_READY_REQ`(Weld_01 Ready) / `M70015 PC_WELD_READY_ACK`. PLC ON 시 Recipe/Page 확인 팝업 + Auto→Manual + Resume 트리거. (기존 M70516/M70016은 `..._TRIGGER_*`로 rename)
- **레시피**: PTOP는 **레시피 21,22(가접용)** 에 가접 용접용 시퀀스 수정이 들어감. (가변/가접은 레시피 추가로 처리, 시퀀스 본체 무수정 방침)
- **최종 방침(2026-06-22 현장 확정)**:
- 가접 실행은 bit15 Recipe/Page 확인 팝업+Resume 같은 복잡한 방식 대신 **"PLC가 Scout TRIGGER 신호를 주면 ScoutImage를 단순 실행"** 으로 단순화. (bit15는 신호 라인으로 유지하되 운용은 단순 트리거)
- **시나리오 상 용접 전 위치에서 ScoutImage 시퀀스 추가**. 추가로 **용접 전/후 이미지를 남기는 방안**으로 요청됨. (작업일지 2026-06-22)
- 연관 메모리: [[uframe-page5-gajeop-recipe]], [[bit15-resume-feature]], [[recipe-page-handshake-redesign]]
@import ./용어정리.md — 협력사/프로젝트 약칭, 가접 용접(Tack Welding) 정의·운용·PLC 연동

15
docs/용어정리.md Normal file
View File

@ -0,0 +1,15 @@
# 용어 정리 (UNICELL PTOP WELDER)
> 협력사: **UBISAM** / 프로젝트: **UNICELL PTOP WELDING**(약칭 **PTOP**)
## 가접 용접 (Tack Welding)
- **정의**: U-Frame 본(메인) 용접 전에, **팩 제품과 프레임 간 밀착이 안 된 자재를 먼저 붙여 고정**하는 예비 용접.
- **이유**: 자재가 떠 있으면(밀착 불량) 본 용접 품질이 안 나오므로, **Align → 가접으로 밀착 → 메인 용접** 순서로 진행.
- **방식**: **Dot(점) 또는 Short Line(짧은 선)** 출사로 미리 몇 점 붙여 밀착.
- **운용**: 기존 AutoRun 시나리오엔 **가접 시나리오가 없었어서 신규 추가**됨. Manual 모드로 전환 후 Recipe 화면에서 **직접 Scout Image** 촬영 → 수동으로 가접(밀착) 용접 수행.
- **PLC 연동**: 이를 위해 PLC와 협의하여 **15번 bit(0x0F)** 추가 — `M70515 PLC_WELD_READY_REQ`(Weld_01 Ready) / `M70015 PC_WELD_READY_ACK`. PLC ON 시 Recipe/Page 확인 팝업 + Auto→Manual + Resume 트리거. (기존 M70516/M70016은 `..._TRIGGER_*`로 rename)
- **레시피**: PTOP는 **레시피 21,22(가접용)** 에 가접 용접용 시퀀스 수정이 들어감. (가변/가접은 레시피 추가로 처리, 시퀀스 본체 무수정 방침)
- **최종 방침(2026-06-22 현장 확정)**:
- 가접 실행은 bit15 Recipe/Page 확인 팝업+Resume 같은 복잡한 방식 대신 **"PLC가 Scout TRIGGER 신호를 주면 ScoutImage를 단순 실행"** 으로 단순화. (bit15는 신호 라인으로 유지하되 운용은 단순 트리거)
- **시나리오 상 용접 전 위치에서 ScoutImage 시퀀스 추가**. 추가로 **용접 전/후 이미지를 남기는 방안**으로 요청됨. (작업일지 2026-06-22)
- 연관 메모리: [[uframe-page5-gajeop-recipe]], [[bit15-resume-feature]], [[recipe-page-handshake-redesign]]