NohDoyeon241104 7c974628d7 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 검사까지 함께 살아나므로
  별건으로 검토한다.
2026-09-02 22:34:17 +09:00

676 lines
32 KiB
C#

using Aurora;
using Aurora.Api;
using Aurora.View;
using AuroraLaser;
using AuroraLaserScanner;
using CoreUnit.Control.UserControl.Canvas;
using CoreUnit.Scanner.UserControl.ScannerCanvas;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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;
using WeldingCommon.Utils;
namespace WeldingCommon.Views
{
public class UCScannerLaserControlViewModel : NotifyPropertyChangedBase
{
private readonly Logger Log = new Logger("UCScannerLaserControlViewModel");
public ICommand CMD_VisibleChanged { get; }
public ICommand CMD_LaserShotOnPercent { get; }
public ICommand CMD_LaserShotOff { get; }
public ICommand CMD_ActivateOnTheFly { get; }
public ICommand CMD_DeactivateOnTheFly { get; }
public ICommand CMD_ResetEncoderValue { get; }
public ICommand CMD_SimulateEncoder { get; }
public ICommand CMD_SimulateEncoderXY { get; }
public ICommand CMD_LaserShotOnWatt { get; }
private readonly IAuroraEnvironment _Aurora;
private readonly LaserConfig _LaserConfig;
private readonly Scanner _Scanner;
private readonly Laser _Laser;
private readonly ScannerConfig _ScannerConfig;
private double _LaserPower1Percent;
public double LaserPower1Percent { get => _LaserPower1Percent; set => SetProperty(ref _LaserPower1Percent, value); }
private double _LaserPower2Percent;
public double LaserPower2Percent { get => _LaserPower2Percent; set => SetProperty(ref _LaserPower2Percent, value); }
private double _LaserPower1Watt;
public double LaserPower1Watt { get => _LaserPower1Watt; set => SetProperty(ref _LaserPower1Watt, value); }
private double _LaserPower2Watt;
public double LaserPower2Watt { get => _LaserPower2Watt; set => SetProperty(ref _LaserPower2Watt, value); }
// RTC5 AnalogOut(파워 지령 전압) readback 표시값 [V]
private double _AnalogOut1Voltage;
public double AnalogOut1Voltage { get => _AnalogOut1Voltage; set => SetProperty(ref _AnalogOut1Voltage, value); }
private double _AnalogOut2Voltage;
public double AnalogOut2Voltage { get => _AnalogOut2Voltage; set => SetProperty(ref _AnalogOut2Voltage, value); }
private uint _LaserPeriod;
public uint LaserPeriod { get => _LaserPeriod; set => SetProperty(ref _LaserPeriod, value); }
private uint _LaserPulseWidth;
public uint LaserPulseWidth { get => _LaserPulseWidth; set => SetProperty(ref _LaserPulseWidth, value); }
private bool _IsUseLaserAutoOffChecked;
public bool IsUseLaserAutoOffChecked { get => _IsUseLaserAutoOffChecked; set => SetProperty(ref _IsUseLaserAutoOffChecked, value); }
// No Laser 모드: 체크 시 Laser On 눌러도 LaserSignalOn(발진) 생략 → 전압만 인가(전압 readback 표시용)
private bool _IsNoLaser;
public bool IsNoLaser { get => _IsNoLaser; set => SetProperty(ref _IsNoLaser, value); }
private double _LaserOnTime;
public double LaserOnTime { get => _LaserOnTime; set => SetProperty(ref _LaserOnTime, value); }
// List1 or List2 를 Scanner 의 상태를 보고 check
private string _BusyStatus;
public string BusyStatus { get => _BusyStatus; set => SetProperty(ref _BusyStatus, value); }
private int _ListInterationCount;
public int ListInterationCount { get => _ListInterationCount; set => SetProperty(ref _ListInterationCount, value); }
private double _RotateCenterX;
public double RotateCenterX { get => _RotateCenterX; set => SetProperty(ref _RotateCenterX, value); }
private double _RotateCenterY;
public double RotateCenterY { get => _RotateCenterY; set => SetProperty(ref _RotateCenterY, value); }
private double _EncoderResolution;
public double EncoderResolution { get => _EncoderResolution; set => SetProperty(ref _EncoderResolution, value); }
private double _EncoderValueAxisX;
public double EncoderValueAxisX { get => _EncoderValueAxisX; set => SetProperty(ref _EncoderValueAxisX, value); }
private double _EncoderValueAxisY;
public double EncoderValueAxisY { get => _EncoderValueAxisY; set => SetProperty(ref _EncoderValueAxisY, value); }
private double _EncoderValueRotate;
public double EncoderValueRotate { get => _EncoderValueRotate; set => SetProperty(ref _EncoderValueRotate, value); }
private double _EncoderValueVelocity;
public double EncoderValueVelocity { get => _EncoderValueVelocity; set => SetProperty(ref _EncoderValueVelocity, value); }
private bool _IsFieldOutX;
public bool IsFieldOutX { get => _IsFieldOutX; set => SetProperty(ref _IsFieldOutX, value); }
private bool _IsFieldOutY;
public bool IsFieldOutY { get => _IsFieldOutY; set => SetProperty(ref _IsFieldOutY, value); }
private bool _IsUseMarkingOnTheFly;
public bool IsUseMarkingOnTheFly { get => _IsUseMarkingOnTheFly; set => SetProperty(ref _IsUseMarkingOnTheFly, value); }
private int _BeforeBusyList = 0;
private int _ListIterationCount = 0;
private DateTime _LastEncRecordTime = DateTime.Now;
private double _LastEncoderRecorded = 0;
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;
public UCScannerLaserControlViewModel(IAuroraEnvironment aurora, string scannerId, bool isUseMarkingOntheFly = false)
{
_Aurora = aurora;
_Scanner = _Aurora.DeviceManager[scannerId] as Scanner;
_Laser = _Aurora.DeviceManager[CommonDeviceIds.Laser] as Laser;
_LaserConfig = _Aurora.DeviceManager.GetDeviceConfig(_Laser.DeviceId) as AuroraLaser.LaserConfig;
_ScannerConfig = _Aurora.DeviceManager.GetDeviceConfig(scannerId) as ScannerConfig;
_SeqTeachingRun = _Aurora.SequenceEntry[CommonSequenceIds.SeqComponentTeachingRun];
IsUseMarkingOnTheFly = isUseMarkingOntheFly;
InitVariable();
CMD_VisibleChanged = new DelegateCommand(VisibleChanged);
CMD_LaserShotOnPercent = new DelegateCommand(PerformCmdLaserShotOnPercent);
CMD_LaserShotOnWatt = new DelegateCommand(PerformCmdLaserShotOnWatt);
CMD_LaserShotOff = new DelegateCommand(PerformCmdLaserShotOff);
CMD_ActivateOnTheFly = new DelegateCommand(PerformCmdActivateOnTheFly);
CMD_DeactivateOnTheFly = new DelegateCommand(PerformCmdDeactivateOnTheFly);
CMD_ResetEncoderValue = new DelegateCommand(PerformCmdResetEncoderValue);
CMD_SimulateEncoder = new DelegateCommand(PerformCmdSimulateEncoder);
CMD_SimulateEncoderXY = new DelegateCommand(PerformCmdSimulateEncoderXY);
_MonitorTimer = new DispatcherTimer();
_MonitorTimer.Tick += OnMonitoring;
_MonitorTimer.Interval = TimeSpan.FromMilliseconds(100);
}
private void InitVariable()
{
LaserPower1Percent = 0;//10;
LaserPower2Percent = 0;
LaserPeriod = 1;
LaserPulseWidth = 1000000;
LaserPower1Watt = 0;//300;
LaserPower2Watt = 0;
_LastEncRecordTime = DateTime.Now;
IsUseLaserAutoOffChecked = true;
}
private void VisibleChanged(object obj)
{
bool visible = (bool)obj;
if (visible) {
//Init_XYLimitPos();
_MonitorTimer.Start();
} else {
// [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."));
_MonitorTimer.Stop();
}
double encoderX, encoderY;
if (_ScannerConfig.OnTheFlyCoordinate == eOntheFlyCoordinate.XY) {
_Scanner.GetEncoder(out encoderX, out encoderY, false);
} else {
_Scanner.GetEncoder(out encoderX, out encoderY, true);
}
TimeSpan encRecordTimeSpan = DateTime.Now.Subtract(_LastEncRecordTime);
_LastEncRecordTime = DateTime.Now;
// CBW Test용 임시 수정
//m_scannerUnit.GetEncoder(out encoderX, out encoderY, true);
EncoderValueAxisX = encoderX;
EncoderValueAxisY = encoderY;
//m_scannerUnit.GetEncoder(out encoderX, out encoderY, true);
//lbRotateEncoderValue.Text = (encoderX).ToString("f3");
EncoderValueRotate = 0;
EncoderValueVelocity = (encoderX - _LastEncoderRecorded) / (encRecordTimeSpan.TotalSeconds);
_LastEncoderRecorded = encoderX;
//// Field 초과 판단
uint markingInfo = 0;
markingInfo = _Scanner.GetMarkingInfo();
// x 범위 초과
if ((markingInfo & 0x0001) == 0x0001 || (markingInfo & 0x0002) == 0x0002) {
IsFieldOutX = true;
} else {
IsFieldOutX = false;
}
// y 범위 초과
if ((markingInfo & 0x0004) == 0x0004 || (markingInfo & 0x0008) == 0x0008) {
IsFieldOutY = true;
} else {
IsFieldOutY = false;
}
// 상태 파악 및 반복성 확인.
uint readStatus = _Scanner.ReadStatus();
string busyStatus;
uint IsList1Busy = readStatus & 16;
uint IsList2Busy = readStatus & 32;
if (IsList1Busy == 16) {
busyStatus = "List1";
if (_BeforeBusyList != 1) {
_ListIterationCount++;
}
_BeforeBusyList = 1;
} else if (IsList2Busy == 32) {
busyStatus = "List2";
if (_BeforeBusyList != 2) {
_ListIterationCount++;
}
_BeforeBusyList = 2;
} else {
busyStatus = "N/A";
}
BusyStatus = busyStatus;
ListInterationCount = _ListIterationCount;
}
private void ReadbackAnalogOut()
{
int raw1 = (_Scanner.GetAnalogOut((uint)eAnalogPortOut.DA1) & 0x000FFFF0) >> 4;
int raw2 = (_Scanner.GetAnalogOut((uint)eAnalogPortOut.DA2) & 0x000FFFF0) >> 4;
// -1 은 Scanner 미활성(IsActivated == false) sentinel
AnalogOut1Voltage = raw1 < 0 ? 0 : raw1 / 4095.0 * 10.0;
AnalogOut2Voltage = raw2 < 0 ? 0 : raw2 / 4095.0 * 10.0;
}
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."));
return;
}
if (!_Laser.GetLaserReady()) {
MessageUtil.ShowWarningToast(string.Format("Laser, Check Laser Ready status."));
return;
}
if (_SeqTeachingRun.RunState == SequenceRunState.Running) {
MessageUtil.ShowWarningToast(string.Format("이미 시퀀스가 Start 상태입니다.", "Laser On"), Log);
return;
}
var currentModelInfo = EquipmentSpecificModelUtil.GetCurrentRecipeEquipmentSpecificModel();
if (currentModelInfo.equipmentSpecificModel == null) {
MessageUtil.ShowWarningToast(string.Format("선택된 model 이 없습니다.", "Laser On"));
return;
}
var intervalTimeBlowOn = 0;
var intervalTimeBlowOff = 0;
if (currentModelInfo.equipmentSpecificModel.UseBlow) {
intervalTimeBlowOn = currentModelInfo.equipmentSpecificModel.IntervalTimeBlowOn;
intervalTimeBlowOff = currentModelInfo.equipmentSpecificModel.IntervalTimeBlowOff;
}
// Object를 만들어
var param = new SeqTeachingRun_InitParam() {
IsUseExternalObjects = true,
ExternalObjects = CreateDotForLaserOn(),
//UseStream = _EquipmentInfo.UseStream,
UseStream = currentModelInfo.equipmentSpecificModel.UseStream,
UseBlow = currentModelInfo.equipmentSpecificModel.UseBlow,
IntervalTimeBlowOn = intervalTimeBlowOn,
IntervalTimeBlowOff = intervalTimeBlowOff,
NeedSetLaserTiming = true,
};
_SeqTeachingRun.Start(param, OnCompleteMarkGrid);
} else {
LaserPowerCalParam powerCalParam = Status.AuroraEnv.SettingEntry[CommonSettingIds.LaserPowerCalParam] as LaserPowerCalParam;
float powerVolt1 = 0, powerVolt2 = 0;
powerCalParam.Initialize(powerCalParam.PowerCalibrationCenterBeamCSVFile, powerCalParam.PowerCalibrationRingBeamCSVFile);
if (_LaserConfig.TruFiberType == eTruFiberType.TruFiberS) {
powerVolt1 = (float)powerCalParam.CalibratePercentToMeasuredVoltage(LaserPower1Percent) * 10;
} else if(_LaserConfig.TruFiberType == eTruFiberType.TruFiberP){
powerVolt1 = (float)powerCalParam.CalibratePercentToMeasuredVoltage(LaserPower1Percent, (uint)LaserPowerCalParam.beamMode.Center) * 10;
powerVolt2 = (float)powerCalParam.CalibratePercentToMeasuredVoltage(LaserPower2Percent, (uint)LaserPowerCalParam.beamMode.Ring) * 10;
} else if (_Laser.Config.TruFiberType == eTruFiberType.CalPercentToMeasuredVoltage) {
powerVolt1 = (float)powerCalParam.CalibratePercentToMeasuredVoltage(LaserPower1Percent, (uint)LaserPowerCalParam.beamMode.Center) * 10;
powerVolt2 = (float)powerCalParam.CalibratePercentToMeasuredVoltage(LaserPower2Percent, (uint)LaserPowerCalParam.beamMode.Ring) * 10;
} else {
powerVolt1 = (float)powerCalParam.CalibratePercentToMeasuredVoltage(LaserPower1Percent, (uint)LaserPowerCalParam.beamMode.Center) * 10;
powerVolt2 = (float)powerCalParam.CalibratePercentToMeasuredVoltage(LaserPower2Percent, (uint)LaserPowerCalParam.beamMode.Ring) * 10;
}
Thread.Sleep(50);
_Scanner.WriteDA((uint)eAnalogPortOut.DA1, powerVolt1, eFunctionType.COMMAND);
_Scanner.WriteDA((uint)eAnalogPortOut.DA2, powerVolt2, eFunctionType.COMMAND);
ReadbackAnalogOut(); // DAC에 실린 AnalogOut 전압을 읽어 UI에 표시 (출사 전)
_Scanner.SetLaserPulseCtrl(LaserPeriod * 1000, LaserPulseWidth);
//_Scanner.SetLaserTiming(LaserPeriod * 1000, LaserPulseWidth, LaserPulseWidth);
//_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()) {
MessageUtil.ShowWarningToast(string.Format("Laser, Scanner is busy."));
return;
}
if (!_Laser.GetLaserReady()) {
MessageUtil.ShowWarningToast(string.Format("Laser, Check Laser Ready status."));
return;
}
if (_SeqTeachingRun.RunState == SequenceRunState.Running) {
MessageUtil.ShowWarningToast(string.Format("이미 시퀀스가 Start 상태입니다.", "Laser On"), Log);
return;
}
var currentModelInfo = EquipmentSpecificModelUtil.GetCurrentRecipeEquipmentSpecificModel();
if (currentModelInfo.equipmentSpecificModel == null) {
MessageUtil.ShowWarningToast(string.Format("선택된 model 이 없습니다.", "Laser On"));
return;
}
var intervalTimeBlowOn = 0;
var intervalTimeBlowOff = 0;
if (currentModelInfo.equipmentSpecificModel.UseBlow) {
intervalTimeBlowOn = currentModelInfo.equipmentSpecificModel.IntervalTimeBlowOn;
intervalTimeBlowOff = currentModelInfo.equipmentSpecificModel.IntervalTimeBlowOff;
}
// Object를 만들어
var param = new SeqTeachingRun_InitParam()
{
IsUseExternalObjects = true,
ExternalObjects = CreateDotForLaserOn(),
//UseStream = _EquipmentInfo.UseStream,
UseStream = currentModelInfo.equipmentSpecificModel.UseStream,
UseBlow = currentModelInfo.equipmentSpecificModel.UseBlow,
IntervalTimeBlowOn = intervalTimeBlowOn,
IntervalTimeBlowOff = intervalTimeBlowOff,
NeedSetLaserTiming = true,
};
_SeqTeachingRun.Start(param, OnCompleteMarkGrid);
} else {
LaserPowerCalParam powerCalParam = Status.AuroraEnv.SettingEntry[CommonSettingIds.LaserPowerCalParam] as LaserPowerCalParam;
float powerVolt1 = 0, powerVolt2 = 0;
powerCalParam.Initialize(powerCalParam.PowerCalibrationCenterBeamCSVFile, powerCalParam.PowerCalibrationRingBeamCSVFile);
if (_LaserConfig.TruFiberType == eTruFiberType.TruFiberS) {
powerVolt1 = (float)powerCalParam.CalibrateWattToMeasuredVoltage(LaserPower1Watt) * 10;
}
else if (_LaserConfig.TruFiberType == eTruFiberType.TruFiberP) {
powerVolt1 = (float)powerCalParam.CalibrateWattToMeasuredVoltage(LaserPower1Watt, (uint)LaserPowerCalParam.beamMode.Center) * 10;
powerVolt2 = (float)powerCalParam.CalibrateWattToMeasuredVoltage(LaserPower2Watt, (uint)LaserPowerCalParam.beamMode.Ring) * 10;
} else if (_Laser.Config.TruFiberType == eTruFiberType.CalPercentToMeasuredVoltage) {
powerVolt1 = (float)powerCalParam.CalibrateWattToMeasuredVoltage(LaserPower1Watt, (uint)LaserPowerCalParam.beamMode.Center) * 10;
powerVolt2 = (float)powerCalParam.CalibrateWattToMeasuredVoltage(LaserPower2Watt, (uint)LaserPowerCalParam.beamMode.Ring) * 10;
} else {
powerVolt1 = (float)powerCalParam.CalibratePercentToMeasuredVoltage(LaserPower1Percent, (uint)LaserPowerCalParam.beamMode.Center) * 10;
powerVolt2 = (float)powerCalParam.CalibratePercentToMeasuredVoltage(LaserPower2Percent, (uint)LaserPowerCalParam.beamMode.Ring) * 10;
}
Thread.Sleep(50);
_Scanner.WriteDA((uint)eAnalogPortOut.DA1, powerVolt1, eFunctionType.COMMAND);
_Scanner.WriteDA((uint)eAnalogPortOut.DA2, powerVolt2, eFunctionType.COMMAND);
ReadbackAnalogOut(); // DAC에 실린 AnalogOut 전압을 읽어 UI에 표시 (출사 전)
_Scanner.SetLaserPulseCtrl(LaserPeriod * 1000, LaserPulseWidth);
//_Scanner.SetLaserTiming(LaserPeriod * 1000, LaserPulseWidth, LaserPulseWidth);
//_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 OnCompleteMarkGrid(SequenceActionResult res)
{
if (res != null) {
if (res.Param is SeqTeachingRun_OutParam result && result.Success == true) {
MessageUtil.ShowSuccessToast(string.Format("Success SeqMarkGrid"));
return;
}
}
MessageUtil.ShowWarningToast(string.Format("Fail SeqMarkGrid"));
}
private void PerformCmdLaserShotOff(object obj)
{
if (IsUseLaserAutoOffChecked) {
_Scanner.StopExecution();
} else {
_Scanner.LaserSignalOff();
}
_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 {
_Scanner.InitFly2D();
// CBW OTW 테스트를 위해 아래는 일단 주석처리
//m_scannerUnit.SetFlyX(m_scannerUnit.ScannerUnitParamObj.MarkingOnTheFlyEncoderResolutionX);
// list 반복 숫자 초기화
_ListIterationCount = 0;
} catch (Exception ex) {
MessageUtil.ShowErrorToast(string.Format($"{ex.Message}"));
}
}
private void PerformCmdActivateOnTheFly(object obj)
{
_Scanner.SetRotateCenter(RotateCenterX, RotateCenterY);
// list 1
_Scanner.SetStartList(eListNo.LIST1);
_Scanner.SetFlyRotate(EncoderResolution);
_Scanner.SetLaserTiming(10, 100, 100); // 아무거나
_Scanner.SetStandby(10, 0); // 아무거나
_Scanner.LaserOnList(LaserOnTime); // 1000이 1초
_Scanner.SetEndList();
// End - list1
_Scanner.ExecuteList(eListNo.LIST1);
}
private void PerformCmdDeactivateOnTheFly(object obj)
{
_Scanner.SetStartList(eListNo.LIST1);
_Scanner.Fly_Return(0, 0);
_Scanner.SetEndList();
_Scanner.ExecuteList(eListNo.LIST1);
}
private void PerformCmdSimulateEncoder(object obj)
{
if (_ToggleSimulateEncoder) {
_Scanner.SimulateEncoder(0);
_ToggleSimulateEncoder = false;
} else {
_ToggleSimulateEncoder = true;
_Scanner.SimulateEncoder(1);
}
}
private void PerformCmdSimulateEncoderXY(object obj)
{
if (_ToggleSimulateEncoder) {
_Scanner.SimulateEncoder(0);
_ToggleSimulateEncoder = false;
} else {
_ToggleSimulateEncoder = true;
_Scanner.SimulateEncoder(3);
}
}
private List<IDrawObject> CreateDotForLaserOn()
{
LaserPowerCalParam powerCalParam = Status.AuroraEnv.SettingEntry[CommonSettingIds.LaserPowerCalParam] as LaserPowerCalParam;
ScannerParam param = new ScannerParam();
param.LaserPower1 = (uint)(LaserPower1Percent / 100 * LaserPowerCalParam.MaxCenterPower);// = LaserPower1; // 변경 필요
param.LaserPower2 = (uint)(LaserPower2Percent / 100 * LaserPowerCalParam.MaxRingPower);// = LaserPower1; // 변경 필요
param.MarkSpeed = 200;
param.JumpSpeed = 1000;
param.SpotShotTime = LaserOnTime; // 변경 필요
param.LaserFrequency = 1000;
param.LaserPulseDuration = 1000;
param.MarkDelay = 200;
param.JumpDelay = 200;
param.PolygonDelay = 200;
ScannerCanvasUserCtrl.ScannerParams[127] = param;
Dot dot = new Dot();
_Scanner.GetXYPos(out double xPos, out double yPos);
dot.P1 = new UnitPoint(xPos, yPos);
dot.Enabled = true;
dot.Selected = true;
dot.ScannerParamNo = 127;
List<IDrawObject> drawObjects = new List<IDrawObject>();
drawObjects.Add(dot);
return drawObjects;
}
// 일단 이건 1 source 에서 가져옴. view 에서 주석처리 된 부분
//private void ApplyAnalogValue(object obj)
//{
// try {
// //DA1, DA2 Combobox 이벤트로 현재값을 AnalogVaule에 보여주고싶으나 해야될게많다..
// //일단 유저가 셋팅만 해서 쓰는 걸로하자..
// if (AnalogPortSelectedIndex == 0) {
// DA1PercentLaser = (float)AnalogValue;
// } else if (AnalogPortSelectedIndex == 1) {
// DA2PercentLaser = (float)AnalogValue;
// } else {
// MessageDialog.Show(Application.Current.MainWindow, "LaserManual", "Check AnalogPortIndex", MessageDialog.Type.Affirmative);
// }
// } catch (Exception ex) {
// Log.E(string.Format($"{ex.Message}"));
// }
//}
}
}