fix: 메뉴얼 모드 Door Open 시 레이저 출사 차단 (ISSUE-260902-01)
## 배경 (현장 사고 2026-09-02)
파워측정 / 파워캘 / 롱텀(1시간 연속출사) 작업 중 Door Open 상태에서 레이저가
출사되었다. 현장 요청: "메뉴얼에서도 Door open 상태에서 레이저 출사 안 되도록
Interlock 필요".
## 원인 (수정 전)
1) SeqComponentTeachingRun.CheckCondition_Interlock()
Door 차단 코드가 '#warning to KSJ > 지워도 무방해보임' 과 함께 전체 주석 처리
되어 있었다. 남은 검사는 HasAlarmLevelHigh() 뿐이라 Door Open 이어도 통과한다.
이 함수가 Manual 경로(Teaching Run / Laser Shot On / Scanner Calibration)의
유일한 출사 게이트였다.
2) UCScannerLaserControlViewModel.PerformCmdLaserShotOnPercent / ...OnWatt
IsUseLaserAutoOffChecked == false 분기(= 파워측정 직접 출사)는 시퀀스를 아예
타지 않고 _Scanner.LaserSignalOn() 을 직접 호출한다. 이 분기에는 Door / EMO /
Busy / LaserReady 어떤 인터락도 없었다. LaserSignalOn 은 자동 오프가 없어
Shot Off 를 누를 때까지 계속 출사되며, 이것이 롱텀 1시간 연속출사의 구현
경로다. 이번 사고의 실제 경로.
3) Door 신호 2계통이 모두 출사를 막지 않고 있었다
- PLC 비트: AlarmManager.IsPlcDoorOpen() 을 호출하는 살아있는 코드가 0곳
(SeqComponentTeachingRun 주석, UCMainAutoRunViewModel 블록 주석)
- IO DI 접점: IODevice.MonitorIOInterfaceMap 이 DoorLockOpen1~4 를 읽지만
타워램프 점등과 EVENT_DOOR_INTERLOCK 발행만 하고, 그 이벤트의 구독자가 없다
## 변경 (수정 후)
[WeldingCommon/Alarm/AlarmManager.cs]
- IsDoorOpen(caller) 신규: PLC 비트(PLC_DOOR_INTERLOCK)와 IO DI 접점
(DoorLockOpen1~4)을 OR 판정. 한쪽 배선/맵이 틀려도 다른 쪽이 잡도록 했다.
- IsIoDoorOpen(), GetDoorStateText() 신규.
- PAIXIO.ReadDiBit 은 비트 1개마다 DI 배열 전체를 읽으므로(PAIXIO.cs:305~307)
80ms TTL 캐시를 두어 100ms 감시 1틱당 실제 읽기 1회로 제한했다.
- IsPlcDoorOpen(): _Plc 지연 조회 보완. 기존에는 AlarmManager 생성 시점에 PLC
등록이 안 끝났으면 _Plc 가 null 로 남아 영구히 false 를 반환 → Door 인터락이
조용히 무력화될 수 있었다.
- PLC/IO 미연결 계통은 판정에서 제외(의도적 fail-open). Door 맵이 없는 타 설비가
이 변경으로 갑자기 출사 불가가 되는 것을 막기 위함. PTOP 는 둘 다 연결되어 있다.
[WeldingCommon/Views/Manual/Scanner/UCScannerLaserControlViewModel.cs]
- PerformCmdLaserShotOnPercent / ...OnWatt 진입부에 Door 게이트 추가
-> 시퀀스 분기와 직접 출사 분기를 모두 커버한다.
- OnMonitoring(100ms)에 MonitorDoorInterlock() 추가: 출사 중 Door 가 열리면
StopExecution + LaserSignalOff + SetStandby 로 강제 차단한다. 시작 게이트만으로는
롱텀 도중 문이 열리는 이번 사고 상황을 막을 수 없어 추가했다.
- VisibleChanged: 직접 출사 중에는 화면을 벗어나도 감시 타이머를 유지한다.
기존처럼 Stop 하면 롱텀 출사 중 Door 감시가 통째로 사라진다.
- 롱텀 추적용 10초 주기 하트비트 로그(Door 상태 + DA1/DA2 전압) 추가.
[ILSOS_PrismaticRD_PTOP/Sequences/Manual/SeqComponentTeachingRun.cs]
- CheckCondition_Interlock(): 주석 처리돼 있던 Door 차단을 IsDoorOpen() 으로 복구.
- DoComponentWelding(): ExecuteList 직전 최종 게이트 추가. 시작 게이트는 시퀀스
진입 시 1회만 돌아, 객체가 여러 개일 때 매 출사 직전 재확인이 필요하다.
## 영향 범위
- SeqAutoRunPTOP 은 자체 ExecuteWelding(SeqAutoRunPTOP.cs:1314)을 사용하며
SeqComponentTeachingRun 을 서브시퀀스로 타지 않는다 -> Auto 생산 동작 변경 없음.
- WeldingCommon 공용 파일을 수정했으나, Door 맵이 없는 설비는 fail-open 으로
기존 동작이 유지된다.
## 남은 사항 (별건)
- 본 변경은 SW 방어이지 안전 인터락이 아니다. Door 감시가 UI 스레드
DispatcherTimer 기반이라 UI 정지 시 함께 멈춘다. Class 4 레이저 기준 최종
차단은 PLC/하드와이어 안전회로로 별도 협의가 필요하다.
- SeqComponentTeachingRun 의 EMO 차단(IsPlcEmoAlarm)도 함께 주석 처리되어 있다.
이번 범위에서 제외했으며, PLC_EMO_STATUS 맵 확인 후 복구 여부를 결정한다.
- UCMainAutoRunViewModel.ApproveAutorunByUserOrAuto() 전체가 블록 주석
(399~431행)이다. Auto 시작 게이트 복구는 Vision Recipe 검사까지 함께 살아나므로
별건으로 검토한다.
This commit is contained in:
parent
d34fad9edb
commit
7c974628d7
@ -278,14 +278,20 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
|
||||
return false;
|
||||
}
|
||||
|
||||
#warning to KSJ > 지워도 무방해보임
|
||||
//boolian = AlarmManager.IsPlcDoorOpen();
|
||||
//if (boolian) {
|
||||
// Log.E($"PLC Door Opened!! Can't Weld Now.");
|
||||
// MessageUtil.ShowWarningToast(string.Format("Close Door ! Clear PLC DoorInterlock!"));
|
||||
// return false;
|
||||
//}
|
||||
// [Door Interlock 2026-09-02] 주석 처리되어 있던 Door 차단을 복구 (ISSUE-260902-01)
|
||||
// 기존: '#warning to KSJ > 지워도 무방해보임' 과 함께 전체 주석 → Manual 경로에서 Door Open 이어도 출사됨.
|
||||
// 변경: PLC 비트 + IO DI 접점 OR 판정(AlarmManager.IsDoorOpen)으로 복구.
|
||||
// IsPlcDoorOpen() 단독이 아니라 OR 을 쓰는 이유는 한쪽 배선/맵이 틀려도 다른 쪽이 잡도록 하기 위함.
|
||||
boolian = AlarmManager.IsDoorOpen("SeqComponentTeachingRun");
|
||||
if (boolian) {
|
||||
Log.E($"[DoorInterlock] Door Open 상태 → 용접(출사) 차단. {AlarmManager.GetDoorStateText()}");
|
||||
MessageUtil.ShowWarningToast(string.Format("Door Open 상태입니다. 문을 닫고 다시 시도하세요. (Laser Interlock)"));
|
||||
return false;
|
||||
}
|
||||
Log.I($"[DoorInterlock] Door Close 확인 → 용접(출사) 허용. {AlarmManager.GetDoorStateText()}");
|
||||
|
||||
// TODO(ISSUE-260902-01): EMO 차단도 함께 주석 처리되어 있음. 이번 변경 범위(Door)에서 제외했으니
|
||||
// PLC_EMO_STATUS 맵 확인 후 별도로 복구 여부 결정할 것.
|
||||
//boolian = AlarmManager.IsPlcEmoAlarm();
|
||||
//if (boolian) {
|
||||
// Log.E($"PLC EMO Pushed!! Can't Weld Now.");
|
||||
@ -612,7 +618,17 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
|
||||
}
|
||||
}
|
||||
|
||||
// [Door Interlock 2026-09-02] 출사 직전 최종 게이트 (ISSUE-260902-01)
|
||||
// CheckInitCondition 의 시작 게이트는 시퀀스 진입 시 1회만 돈다.
|
||||
// 객체가 여러 개면 이 구간이 반복되므로, 매 출사 직전마다 Door 를 다시 확인한다.
|
||||
if (AlarmManager.IsDoorOpen("DoComponentWelding")) {
|
||||
Log.E($"[DoorInterlock] 출사 직전 Door Open 감지 → ExecuteList 중단. {AlarmManager.GetDoorStateText()}");
|
||||
MessageUtil.ShowWarningToast(string.Format("Door Open 상태입니다. 출사를 중단합니다. (Laser Interlock)"));
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTime weldingTime = DateTime.Now;
|
||||
Log.I($"[DoorInterlock] 출사 직전 Door Close 확인 → ExecuteList 실행 (ListNo:{_WeldingScannerListNo})");
|
||||
_Scanner.ExecuteList(_WeldingScannerListNo);
|
||||
_Scanner.WaitForListTaskCompleted(1000);//(_SystemParameter.WeldLinesTaskTimeOut);
|
||||
_MeasurementSpanTime = DateTime.Now.Subtract(weldingTime);
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
using Aurora.Api;
|
||||
using Aurora.Api.Manager;
|
||||
using Aurora.Framework.Alarm;
|
||||
using AuroraIO;
|
||||
using AuroraIO.Vendor.PAIX;
|
||||
using AuroraPLC;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -249,8 +251,133 @@ namespace WeldingCommon.Alarm
|
||||
|
||||
public static bool IsPlcDoorOpen()
|
||||
{
|
||||
if (_Plc == null) return false;
|
||||
else return _Plc.ReadBit(eCommonBitInput.PLC_DOOR_INTERLOCK).data;
|
||||
// [Door Interlock 2026-09-02] 지연 조회 보완.
|
||||
// 기존에는 AlarmManager 생성 시점에 PLC Device 등록이 끝나지 않았으면 _Plc 가 null 로 남고,
|
||||
// 이후 영구히 false 를 반환해 Door 인터락이 조용히 무력화될 수 있었다.
|
||||
var plc = ResolvePlc();
|
||||
if (plc == null) return false;
|
||||
else return plc.ReadBit(eCommonBitInput.PLC_DOOR_INTERLOCK).data;
|
||||
}
|
||||
|
||||
#region [Door Interlock 2026-09-02] 메뉴얼 출사 Door 인터락 (ISSUE-260902-01)
|
||||
// 배경(현장 사고 2026-09-02):
|
||||
// 파워측정 / 파워캘 / 롱텀(1시간 연속출사) 작업 중 Door Open 상태에서도 레이저가 출사되었다.
|
||||
// 해당 경로(UCScannerLaserControlViewModel 의 직접 출사 분기)에는 인터락이 전혀 없었고,
|
||||
// SeqComponentTeachingRun 의 Door 차단은 주석 처리되어 무력화된 상태였다.
|
||||
//
|
||||
// 판정 원칙:
|
||||
// Door 신호는 2계통(PLC 비트 PLC_DOOR_INTERLOCK, IO DI 접점 DoorLockOpen1~4)이며
|
||||
// "어느 한쪽이라도 Open 이면 Open" 으로 본다(OR). 한쪽 배선/맵이 틀려도 다른 쪽이 잡도록 하기 위함.
|
||||
//
|
||||
// 미연결 처리:
|
||||
// PLC / IO 가 미연결·미활성이면 해당 계통은 판정에서 제외한다(false).
|
||||
// Door 맵이 없는 타 설비가 이 변경으로 갑자기 출사 불가가 되는 것을 막기 위한 의도적 fail-open 이다.
|
||||
// PTOP 는 PLC·IO 가 모두 연결되어 있으므로 실사용상 fail-open 으로 빠지지 않는다.
|
||||
|
||||
private static IODevice _IO;
|
||||
|
||||
// 100ms 주기 감시에서 호출되므로, 상태가 바뀔 때만 로그를 남겨 로그 폭주를 막는다.
|
||||
private static readonly Dictionary<string, bool> _DoorLogState = new Dictionary<string, bool>();
|
||||
|
||||
private static PLCController ResolvePlc()
|
||||
{
|
||||
// 생성 시점에 Device 등록이 끝나지 않았을 수 있어 지연 조회로 보완한다.
|
||||
if (_Plc == null && _Instance != null) {
|
||||
_Plc = _Instance._DeviceManager?.GetDevice(CommonDeviceIds.PLCDevice) as PLCController;
|
||||
if (_Plc != null) AlarmLog.I("[DoorInterlock] PLCController 지연 조회 성공");
|
||||
}
|
||||
return _Plc;
|
||||
}
|
||||
|
||||
private static IODevice ResolveIO()
|
||||
{
|
||||
if (_IO == null && _Instance != null) {
|
||||
_IO = _Instance._DeviceManager?.GetDevice(CommonDeviceIds.IODevice) as IODevice;
|
||||
if (_IO != null) AlarmLog.I("[DoorInterlock] IODevice 지연 조회 성공");
|
||||
}
|
||||
return _IO;
|
||||
}
|
||||
|
||||
// PAIXIO.ReadDiBit 은 비트 1개를 읽을 때마다 DI 배열 전체를 조회한다(PAIXIO.cs:305~307).
|
||||
// 출사 중 감시가 100ms 주기로 돌기 때문에 매 호출마다 4비트를 읽으면 IO 보드/UI 스레드에 부담이 된다.
|
||||
// → 짧은 TTL 캐시를 두어 감시 1틱당 1회만 실제로 읽도록 한다.
|
||||
private const int DOOR_CACHE_MS = 80;
|
||||
private static bool _DoorPlcOpenCache;
|
||||
private static bool _DoorIoOpenCache;
|
||||
private static DateTime _DoorCacheTime = DateTime.MinValue;
|
||||
|
||||
private static void RefreshDoorCache()
|
||||
{
|
||||
if (DateTime.Now.Subtract(_DoorCacheTime).TotalMilliseconds < DOOR_CACHE_MS) return;
|
||||
|
||||
_DoorCacheTime = DateTime.Now;
|
||||
_DoorPlcOpenCache = IsPlcDoorOpen();
|
||||
_DoorIoOpenCache = ReadIoDoorOpenRaw();
|
||||
}
|
||||
|
||||
private static bool ReadIoDoorOpenRaw()
|
||||
{
|
||||
var io = ResolveIO();
|
||||
if (io == null || io.IsActivated == false || io._iOUnit == null) return false;
|
||||
|
||||
try {
|
||||
return io.ReadDI((int)PAIXIO.ePDIAddress.DoorLockOpen1)
|
||||
|| io.ReadDI((int)PAIXIO.ePDIAddress.DoorLockOpen2)
|
||||
|| io.ReadDI((int)PAIXIO.ePDIAddress.DoorLockOpen3)
|
||||
|| io.ReadDI((int)PAIXIO.ePDIAddress.DoorLockOpen4);
|
||||
} catch (Exception ex) {
|
||||
AlarmLog.E($"[DoorInterlock] IO DI 읽기 실패 → IO 계통 판정 제외: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IO DI 접점(DoorLockOpen1~4) 기준 Door Open 여부.
|
||||
/// IO 미연결/미활성이면 판정 불가로 보고 false 를 반환한다.
|
||||
/// </summary>
|
||||
public static bool IsIoDoorOpen()
|
||||
{
|
||||
RefreshDoorCache();
|
||||
return _DoorIoOpenCache;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PLC 비트와 IO DI 접점을 OR 하여 Door Open 여부를 판정한다.
|
||||
/// caller 는 로그 식별용 호출부 이름.
|
||||
/// </summary>
|
||||
public static bool IsDoorOpen(string caller = "")
|
||||
{
|
||||
RefreshDoorCache();
|
||||
|
||||
bool plcOpen = _DoorPlcOpenCache;
|
||||
bool ioOpen = _DoorIoOpenCache;
|
||||
bool open = plcOpen || ioOpen;
|
||||
|
||||
if (IsDoorStateChanged(caller, open)) {
|
||||
if (open)
|
||||
AlarmLog.W($"[DoorInterlock][{caller}] Door OPEN 감지 → 이후 레이저 출사 차단 (PLC={plcOpen}, IO_DI={ioOpen})");
|
||||
else
|
||||
AlarmLog.I($"[DoorInterlock][{caller}] Door CLOSE 복귀 → 출사 허용 (PLC={plcOpen}, IO_DI={ioOpen})");
|
||||
}
|
||||
|
||||
return open;
|
||||
}
|
||||
|
||||
/// <summary>차단 로그에 붙일 Door 신호 상세 문자열. 캐시값을 쓰므로 추가 IO 읽기가 없다.</summary>
|
||||
public static string GetDoorStateText()
|
||||
{
|
||||
RefreshDoorCache();
|
||||
return $"PLC비트(PLC_DOOR_INTERLOCK)={_DoorPlcOpenCache}, IO접점(DoorLockOpen1~4)={_DoorIoOpenCache}, " +
|
||||
$"PLC연결={(ResolvePlc() != null)}, IO연결={(ResolveIO() != null)}";
|
||||
}
|
||||
|
||||
private static bool IsDoorStateChanged(string caller, bool open)
|
||||
{
|
||||
string key = caller ?? string.Empty;
|
||||
if (_DoorLogState.TryGetValue(key, out bool prev) && prev == open) return false;
|
||||
_DoorLogState[key] = open;
|
||||
return true;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using WeldingCommon.Alarm;
|
||||
using WeldingCommon.Models;
|
||||
using WeldingCommon.Sequences;
|
||||
using WeldingCommon.Settings;
|
||||
@ -113,6 +114,12 @@ namespace WeldingCommon.Views
|
||||
|
||||
private bool _ToggleSimulateEncoder = false; // 왜 있는지는 모르겠음.
|
||||
|
||||
// [Door Interlock 2026-09-02] 직접 출사(LaserSignalOn) 진행 여부.
|
||||
// LaserSignalOn 은 자동 오프가 없어 Shot Off 를 누를 때까지 계속 출사되므로(롱텀 1시간 출사가 이 경로),
|
||||
// 출사 중 Door 가 열리면 강제 OFF 해야 한다. 그 판단에 쓰는 래치.
|
||||
private bool _IsManualEmitting = false;
|
||||
private DateTime _LastEmitHeartbeat = DateTime.MinValue;
|
||||
|
||||
private readonly DispatcherTimer _MonitorTimer;
|
||||
private ISequence _SeqTeachingRun;
|
||||
|
||||
@ -170,12 +177,22 @@ namespace WeldingCommon.Views
|
||||
//Init_XYLimitPos();
|
||||
_MonitorTimer.Start();
|
||||
} else {
|
||||
_MonitorTimer.Stop();
|
||||
// [Door Interlock 2026-09-02] 직접 출사(자동 오프 없음) 중이면 화면을 벗어나도 타이머를 멈추지 않는다.
|
||||
// 멈추면 롱텀 출사 도중 Door 감시가 통째로 사라지기 때문. Shot Off 시점에 정리된다.
|
||||
if (_IsManualEmitting) {
|
||||
Log.W("[DoorInterlock] 직접 출사 중이므로 화면 이탈에도 Door 감시 타이머를 유지합니다.");
|
||||
} else {
|
||||
_MonitorTimer.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMonitoring(object sender, EventArgs e)
|
||||
{
|
||||
// [Door Interlock 2026-09-02] 출사 중 Door 감시를 최우선으로 수행한다.
|
||||
// 아래 스캐너 읽기에서 예외가 나더라도 차단 로직은 먼저 돌아야 하므로 맨 앞에 둔다.
|
||||
MonitorDoorInterlock();
|
||||
|
||||
if (_Scanner.IsActivated == false) {
|
||||
Log.D(string.Format("[OnMonitoring] Check Scanner device Connect. ScannerLaserControl view OnMonitoring Stop"));
|
||||
MessageUtil.ShowWarningToast(string.Format("Check Scanner device Connect."));
|
||||
@ -261,6 +278,9 @@ namespace WeldingCommon.Views
|
||||
|
||||
private void PerformCmdLaserShotOnPercent(object obj)
|
||||
{
|
||||
// [Door Interlock 2026-09-02] 두 분기(시퀀스 / 직접 출사) 공통 게이트
|
||||
if (!CheckDoorInterlockBeforeEmit("LaserShotOn(Percent)")) return;
|
||||
|
||||
if (IsUseLaserAutoOffChecked) {
|
||||
if (_Scanner.GetBusyStatus()) {
|
||||
MessageUtil.ShowWarningToast(string.Format("Laser, Scanner is busy."));
|
||||
@ -334,11 +354,21 @@ namespace WeldingCommon.Views
|
||||
//_Scanner.SetStandby(LaserPeriod * 1000, LaserPulseWidth);
|
||||
if (!IsNoLaser) {
|
||||
_Scanner.LaserSignalOn(); // No Laser면 발진 신호 생략 → 전압만 인가
|
||||
|
||||
// [Door Interlock 2026-09-02] 자동 오프가 없는 연속 출사 → 출사 중 Door 감시 대상으로 등록
|
||||
_IsManualEmitting = true;
|
||||
_LastEmitHeartbeat = DateTime.Now;
|
||||
Log.I($"[DoorInterlock] 직접 출사 시작(자동 오프 없음, Shot Off 까지 계속 출사). {AlarmManager.GetDoorStateText()}");
|
||||
Log.W("[DoorInterlock] 출사 중 Door 감시는 이 화면이 떠 있는 동안에만 동작합니다(_MonitorTimer). 롱텀 출사 중 화면 이탈 금지.");
|
||||
} else {
|
||||
Log.I("[DoorInterlock] No Laser 체크됨 → 발진 신호 생략(전압만 인가). 출사 감시 대상 아님.");
|
||||
}
|
||||
}
|
||||
}
|
||||
private void PerformCmdLaserShotOnWatt(object obj)
|
||||
{
|
||||
// [Door Interlock 2026-09-02] 두 분기(시퀀스 / 직접 출사) 공통 게이트
|
||||
if (!CheckDoorInterlockBeforeEmit("LaserShotOn(Watt)")) return;
|
||||
|
||||
if (IsUseLaserAutoOffChecked) {
|
||||
if (_Scanner.GetBusyStatus()) {
|
||||
@ -415,6 +445,14 @@ namespace WeldingCommon.Views
|
||||
//_Scanner.SetStandby(LaserPeriod * 1000, LaserPulseWidth);
|
||||
if (!IsNoLaser) {
|
||||
_Scanner.LaserSignalOn(); // No Laser면 발진 신호 생략 → 전압만 인가
|
||||
|
||||
// [Door Interlock 2026-09-02] 자동 오프가 없는 연속 출사 → 출사 중 Door 감시 대상으로 등록
|
||||
_IsManualEmitting = true;
|
||||
_LastEmitHeartbeat = DateTime.Now;
|
||||
Log.I($"[DoorInterlock] 직접 출사 시작(자동 오프 없음, Shot Off 까지 계속 출사). {AlarmManager.GetDoorStateText()}");
|
||||
Log.W("[DoorInterlock] 출사 중 Door 감시는 이 화면이 떠 있는 동안에만 동작합니다(_MonitorTimer). 롱텀 출사 중 화면 이탈 금지.");
|
||||
} else {
|
||||
Log.I("[DoorInterlock] No Laser 체크됨 → 발진 신호 생략(전압만 인가). 출사 감시 대상 아님.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -439,8 +477,88 @@ namespace WeldingCommon.Views
|
||||
}
|
||||
|
||||
_Scanner.SetStandby(10, 0);
|
||||
|
||||
// [Door Interlock 2026-09-02] 출사 종료 → 감시 래치 해제
|
||||
if (_IsManualEmitting) {
|
||||
Log.I("[DoorInterlock] Shot Off 실행 → 직접 출사 종료, 출사 중 Door 감시 해제");
|
||||
_IsManualEmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
#region [Door Interlock 2026-09-02] 메뉴얼 출사 Door 인터락 (ISSUE-260902-01)
|
||||
// 배경: 파워측정/파워캘/롱텀(1시간 연속출사) 중 Door Open 상태에서 레이저가 출사되어 현장 사고 발생.
|
||||
// 아래 else 분기(직접 출사)에는 Door/EMO/Busy/LaserReady 어떤 인터락도 없었다.
|
||||
|
||||
/// <summary>
|
||||
/// 출사 시작 전 Door 인터락. Open 이면 출사를 막고 false 를 반환한다.
|
||||
/// </summary>
|
||||
private bool CheckDoorInterlockBeforeEmit(string cmdName)
|
||||
{
|
||||
if (!AlarmManager.IsDoorOpen("ScannerLaserControl")) {
|
||||
Log.I($"[DoorInterlock][{cmdName}] Door CLOSE 확인 → 출사 허용. {AlarmManager.GetDoorStateText()}");
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.E($"[DoorInterlock][{cmdName}] Door OPEN 상태 → 레이저 출사 차단. {AlarmManager.GetDoorStateText()}");
|
||||
MessageUtil.ShowWarningToast("Door Open 상태입니다. 문을 닫고 다시 시도하세요. (Laser Interlock)");
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 출사 중 Door 가 열렸을 때의 강제 차단.
|
||||
/// 안전 경로이므로 어느 분기로 출사 중이든 확실히 끊도록 StopExecution / LaserSignalOff 를 모두 호출한다.
|
||||
/// </summary>
|
||||
private void ForceLaserOffByDoorOpen(string reason)
|
||||
{
|
||||
Log.E($"[DoorInterlock] 출사 중 Door OPEN 감지 → 레이저 강제 OFF 실행 ({reason}). {AlarmManager.GetDoorStateText()}");
|
||||
|
||||
try {
|
||||
_Scanner.StopExecution(); // 시퀀스/리스트 실행 중단
|
||||
} catch (Exception ex) {
|
||||
Log.E($"[DoorInterlock] StopExecution 실패: {ex.Message}");
|
||||
}
|
||||
|
||||
try {
|
||||
_Scanner.LaserSignalOff(); // 직접 출사(LaserSignalOn) 중단
|
||||
} catch (Exception ex) {
|
||||
Log.E($"[DoorInterlock] LaserSignalOff 실패: {ex.Message}");
|
||||
}
|
||||
|
||||
try {
|
||||
_Scanner.SetStandby(10, 0);
|
||||
} catch (Exception ex) {
|
||||
Log.E($"[DoorInterlock] SetStandby 실패: {ex.Message}");
|
||||
}
|
||||
|
||||
_IsManualEmitting = false;
|
||||
Log.E("[DoorInterlock] 레이저 강제 OFF 완료. 재출사하려면 문을 닫고 Shot On 을 다시 누르십시오.");
|
||||
MessageUtil.ShowErrorToast("Door Open 감지 → 레이저를 강제 OFF 했습니다. (Laser Interlock)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 100ms 주기 감시. 출사 중 Door 가 열리면 즉시 차단한다.
|
||||
/// 롱텀(1시간 연속출사) 도중 문이 열리는 상황을 잡기 위한 것으로, 시작 게이트만으로는 막을 수 없다.
|
||||
/// </summary>
|
||||
private void MonitorDoorInterlock()
|
||||
{
|
||||
bool doorOpen = AlarmManager.IsDoorOpen("ScannerLaserControl");
|
||||
bool seqRunning = _SeqTeachingRun != null && _SeqTeachingRun.RunState == SequenceRunState.Running;
|
||||
bool emitting = _IsManualEmitting || seqRunning;
|
||||
|
||||
if (doorOpen && emitting) {
|
||||
ForceLaserOffByDoorOpen(_IsManualEmitting ? "직접 출사 중" : "TeachingRun 시퀀스 실행 중");
|
||||
return;
|
||||
}
|
||||
|
||||
// 롱텀 출사 추적용 하트비트(10초 주기). 1시간 출사 구간의 로그 공백을 없애기 위한 것.
|
||||
if (_IsManualEmitting && DateTime.Now.Subtract(_LastEmitHeartbeat).TotalSeconds >= 10) {
|
||||
_LastEmitHeartbeat = DateTime.Now;
|
||||
Log.I($"[DoorInterlock] 직접 출사 중... Door={(doorOpen ? "OPEN" : "CLOSE")}, " +
|
||||
$"DA1={AnalogOut1Voltage:F3}V, DA2={AnalogOut2Voltage:F3}V, {AlarmManager.GetDoorStateText()}");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void PerformCmdResetEncoderValue(object obj)
|
||||
{
|
||||
try {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user