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>
This commit is contained in:
parent
48befb60b2
commit
041c15550c
@ -15,6 +15,7 @@ using CoreUnit.Scanner.UserControl.ScannerCanvas;
|
|||||||
using DB;
|
using DB;
|
||||||
using GUI;
|
using GUI;
|
||||||
using Hermes;
|
using Hermes;
|
||||||
|
using ILSOS_PrismaticRD_PTOP.Alarm;
|
||||||
using ILSOS_PrismaticRD_PTOP.Device;
|
using ILSOS_PrismaticRD_PTOP.Device;
|
||||||
using ILSOS_PrismaticRD_PTOP.Device.IO;
|
using ILSOS_PrismaticRD_PTOP.Device.IO;
|
||||||
using ILSOS_PrismaticRD_PTOP.Settings;
|
using ILSOS_PrismaticRD_PTOP.Settings;
|
||||||
@ -567,6 +568,50 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
|
|||||||
Status.SetWorkingPosition(eWorkingPosition.EXECUTING_WELD);
|
Status.SetWorkingPosition(eWorkingPosition.EXECUTING_WELD);
|
||||||
|
|
||||||
Log.I($"Welding List Space : {_Scanner.GetListSpace()}");
|
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;
|
DateTime weldingTime = DateTime.Now;
|
||||||
_Scanner.ExecuteList(_WeldingScannerListNo);
|
_Scanner.ExecuteList(_WeldingScannerListNo);
|
||||||
_Scanner.WaitForListTaskCompleted(1000);//(_SystemParameter.WeldLinesTaskTimeOut);
|
_Scanner.WaitForListTaskCompleted(1000);//(_SystemParameter.WeldLinesTaskTimeOut);
|
||||||
|
|||||||
@ -767,11 +767,22 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
|
|||||||
bool okFront = Math.Abs(thetaDeg - thetaSetting.HeadThetaFront) <= thetaSetting.HeadThetaTolerance;
|
bool okFront = Math.Abs(thetaDeg - thetaSetting.HeadThetaFront) <= thetaSetting.HeadThetaTolerance;
|
||||||
bool okDown = Math.Abs(thetaDeg - thetaSetting.HeadThetaDown) <= 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})");
|
Log.I($"[ExecuteWelding] HeadTheta raw={thetaRaw}, deg={thetaDeg:F1} (Front={thetaSetting.HeadThetaFront}/Down={thetaSetting.HeadThetaDown}/±{thetaSetting.HeadThetaTolerance})");
|
||||||
|
// ① 안전 게이트(기존): 안전 구역(Front/Down 부근) 아니면 차단
|
||||||
if (!okFront && !okDown)
|
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;
|
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)
|
if (blocked)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -98,6 +98,7 @@
|
|||||||
<RowDefinition Height="auto"></RowDefinition>
|
<RowDefinition Height="auto"></RowDefinition>
|
||||||
<RowDefinition Height="auto"></RowDefinition>
|
<RowDefinition Height="auto"></RowDefinition>
|
||||||
<RowDefinition Height="auto"></RowDefinition>
|
<RowDefinition Height="auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="auto"></RowDefinition>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="1*"></ColumnDefinition>
|
<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}"/>
|
<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>
|
</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>
|
</Grid>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
|
|||||||
@ -82,6 +82,40 @@ namespace WeldingCommon.Views
|
|||||||
private bool _IsObjectSelectAll;
|
private bool _IsObjectSelectAll;
|
||||||
public bool IsObjectSelectAll { get => _IsObjectSelectAll; set => SetProperty(ref _IsObjectSelectAll, value); }
|
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 분리 (같은 행 / 행 전환)
|
// ScoutNormal 행 전환 sleep 분리 (같은 행 / 행 전환)
|
||||||
private int _ScoutSameRowSleepMs;
|
private int _ScoutSameRowSleepMs;
|
||||||
public int ScoutSameRowSleepMs
|
public int ScoutSameRowSleepMs
|
||||||
@ -135,6 +169,8 @@ namespace WeldingCommon.Views
|
|||||||
|
|
||||||
InitVariable();
|
InitVariable();
|
||||||
|
|
||||||
|
LoadLaserInterlock();
|
||||||
|
|
||||||
CMD_VisibleChanged = new DelegateCommand(VisibleChanged);
|
CMD_VisibleChanged = new DelegateCommand(VisibleChanged);
|
||||||
|
|
||||||
CMD_RunScoutNormal = new DelegateCommand(PerformCmdRunScoutNormal);
|
CMD_RunScoutNormal = new DelegateCommand(PerformCmdRunScoutNormal);
|
||||||
@ -154,6 +190,14 @@ namespace WeldingCommon.Views
|
|||||||
.ObserveOnDispatcher()
|
.ObserveOnDispatcher()
|
||||||
.Subscribe(OnSettingReloaded);
|
.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 = new DispatcherTimer();
|
||||||
_MonitorTimer.Tick += OnMonitoring;
|
_MonitorTimer.Tick += OnMonitoring;
|
||||||
_MonitorTimer.Interval = TimeSpan.FromMilliseconds(100);
|
_MonitorTimer.Interval = TimeSpan.FromMilliseconds(100);
|
||||||
@ -182,6 +226,7 @@ namespace WeldingCommon.Views
|
|||||||
{
|
{
|
||||||
bool visible = (bool)obj;
|
bool visible = (bool)obj;
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
LoadLaserInterlock(); // 패널 표시 시 현재 활성 레시피의 체크값 반영
|
||||||
_MonitorTimer.Start();
|
_MonitorTimer.Start();
|
||||||
} else {
|
} else {
|
||||||
_MonitorTimer.Stop();
|
_MonitorTimer.Stop();
|
||||||
|
|||||||
@ -253,6 +253,7 @@
|
|||||||
<DependentUpon>SequenceDialog.xaml</DependentUpon>
|
<DependentUpon>SequenceDialog.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Settings\LaserPowerCalParam.cs" />
|
<Compile Include="Settings\LaserPowerCalParam.cs" />
|
||||||
|
<Compile Include="Settings\RecipeLaserInterlock.cs" />
|
||||||
<Compile Include="Settings\SequenceSetting.cs" />
|
<Compile Include="Settings\SequenceSetting.cs" />
|
||||||
<Compile Include="Settings\SettingRmsDataList.cs" />
|
<Compile Include="Settings\SettingRmsDataList.cs" />
|
||||||
<Compile Include="Settings\SettingUploadDataList.cs" />
|
<Compile Include="Settings\SettingUploadDataList.cs" />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user