From 041c15550ceb7aae63f9b053d808c9c87a7abd6b Mon Sep 17 00:00:00 2001 From: to3796zw Date: Thu, 25 Jun 2026 22:30:57 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A0=88=EC=8B=9C=ED=94=BC=EB=B3=84=20?= =?UTF-8?q?Front/Down=20=EB=A0=88=EC=9D=B4=EC=A0=80=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EB=9D=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Manual/SeqComponentTeachingRun.cs | 45 +++++++++++ .../Sequences/SeqAutoRunPTOP.cs | 13 ++- .../Settings/RecipeLaserInterlock.cs | 79 +++++++++++++++++++ .../UCScannerObjectTeachingControlBox.xaml | 10 +++ ...cannerObjectTeachingControlBoxViewModel.cs | 45 +++++++++++ .../WeldingCommon/WeldingCommon.csproj | 1 + 6 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 WeldingComponents/WeldingCommon/Settings/RecipeLaserInterlock.cs diff --git a/Machines/ILSOS_PrismaticRD_PTOP/Sequences/Manual/SeqComponentTeachingRun.cs b/Machines/ILSOS_PrismaticRD_PTOP/Sequences/Manual/SeqComponentTeachingRun.cs index ca1b89e..e0d7af6 100644 --- a/Machines/ILSOS_PrismaticRD_PTOP/Sequences/Manual/SeqComponentTeachingRun.cs +++ b/Machines/ILSOS_PrismaticRD_PTOP/Sequences/Manual/SeqComponentTeachingRun.cs @@ -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(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); diff --git a/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs b/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs index 4a132b0..f28d22f 100644 --- a/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs +++ b/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs @@ -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) { diff --git a/WeldingComponents/WeldingCommon/Settings/RecipeLaserInterlock.cs b/WeldingComponents/WeldingCommon/Settings/RecipeLaserInterlock.cs new file mode 100644 index 0000000..4ac2f7b --- /dev/null +++ b/WeldingComponents/WeldingCommon/Settings/RecipeLaserInterlock.cs @@ -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() ?? 0) == 1; + result.Down = (node["Down"]?.Value() ?? 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)); + } + } +} diff --git a/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBox.xaml b/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBox.xaml index 016dcf1..64bc148 100644 --- a/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBox.xaml +++ b/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBox.xaml @@ -98,6 +98,7 @@ + @@ -158,6 +159,15 @@