using Aurora; using Aurora.Api; using Aurora.View; using AuroraIO; 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; using static WeldingCommon.Devices.Laser.PAIX_PrismaticScanner; // 260824 IO 채널 enum 단일 소스 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; // [출사 표시 2026-09-04] 레이저 출사중 램프 / 출사 조건 로그용 IO 핸들 private IODevice _IODevice; private int _EmissionTick = 0; 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(); // [출사 표시 2026-09-04] 램프 갱신도 아래 스캐너 읽기보다 먼저 한다. // 스캐너에서 예외가 나더라도 출사 여부 표시는 살아 있어야 한다. RefreshEmissionIndicator(); 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) { // [출사 표시 2026-09-04] 차단 여부와 무관하게 누른 시점의 조건을 먼저 남긴다. LogLaserCondition("ShotOn(Percent)"); // [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) { // [출사 표시 2026-09-04] 차단 여부와 무관하게 누른 시점의 조건을 먼저 남긴다. LogLaserCondition("ShotOn(Watt)"); // [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; } // [출사 표시 2026-09-04] Off 직후 조건 스냅샷. DI9_ALARM / DO9_REMOTE 가 실제로 떨어졌는지 확인용. LogLaserCondition("ShotOff"); } #region [Door Interlock 2026-09-02] 메뉴얼 출사 Door 인터락 (ISSUE-260902-01) // 배경: 파워측정/파워캘/롱텀(1시간 연속출사) 중 Door Open 상태에서 레이저가 출사되어 현장 사고 발생. // 아래 else 분기(직접 출사)에는 Door/EMO/Busy/LaserReady 어떤 인터락도 없었다. /// /// 출사 시작 전 Door 인터락. Open 이면 출사를 막고 false 를 반환한다. /// 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; } /// /// 출사 중 Door 가 열렸을 때의 강제 차단. /// 안전 경로이므로 어느 분기로 출사 중이든 확실히 끊도록 StopExecution / LaserSignalOff 를 모두 호출한다. /// 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 을 다시 누르십시오."); // [출사 표시 2026-09-04] 강제 OFF 가 실제로 먹었는지 확인용. // DI Laser_EMISSION_ON 이 실제 발진 피드백이므로(260904 현장 실측), // 이 로그에서 출사가 끊겼는지를 신호로 증명할 수 있다. LogLaserCondition("DoorForceOff"); MessageUtil.ShowErrorToast("Door Open 감지 → 레이저를 강제 OFF 했습니다. (Laser Interlock)"); } /// /// 100ms 주기 감시. 출사 중 Door 가 열리면 즉시 차단한다. /// 롱텀(1시간 연속출사) 도중 문이 열리는 상황을 잡기 위한 것으로, 시작 게이트만으로는 막을 수 없다. /// 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()}"); // [출사 표시 2026-09-04] 롱텀 구간에도 조건 스냅샷을 같은 주기로 남긴다. LogLaserCondition("Heartbeat"); } } #endregion #region [출사 표시 2026-09-04] 레이저 출사중 표시 + 출사 조건 로그 (요청 260904-01) // 요청: 메뉴얼 화면에서 레이저가 실제로 출사 중인지 눈으로 알 수 있게 할 것. // 출사가 안 될 때 무엇이 걸렸는지도 남길 것(화면 아닌 로그로 — 사용자 확정 260904). // // 램프 판정 신호 2개: // DI Laser_EMISSION_ON (10) : 실제 발진 피드백. // ★ 260904 현장 실측 — 이 신호가 들어올 때 실제로 레이저가 출사 중이었다. // LaserIOControl.cs 의 "DO Laser_REMOTE 의 에코" 주석은 오기이며 함께 정정했다. // DO Laser_REMOTE (9) : External Start = Emission 게이트. // 260826 부터 레시피 화면 "Laser Ready" 표시가 쓰는 비트로, 현장에서 이미 검증되어 // 돌고 있는 기존 표시 근거다. DI 가 어떻게 뜰지 확신이 없으므로 기존 신호를 빼지 않고 // 둘을 함께 본다(사용자 지시). // // 문구는 3단. 단순 OR 로 "출사중" 이라고만 쓰면 Laser Ready 버튼만 켜 둔 상태에서도 // 출사중으로 보이기 때문이다. // DI ON → "레이저 출사중" (적색) // DI OFF, DO ON → "출사 대기 (Remote ON)" (주황) // 둘 다 OFF → "OFF" (회색) // // ※ 표시·로그 전용이다. 출사 차단 로직은 한 줄도 바꾸지 않았다. // (차단은 260902 Door 인터락이 유일. 알람/EMO 는 표시·기록만 하고 막지 않는다.) private const string COLOR_EMISSION_ON = "#C62828"; // 적색 - 실제 출사중 private const string COLOR_EMISSION_WAIT = "#EF6C00"; // 주황 - Remote ON(출사 대기) private const string COLOR_EMISSION_OFF = "#3E3E42"; // 회색 - OFF private const string COLOR_EMISSION_NA = "#2A2A2E"; // IO 미연결/읽기실패 // 100ms 타이머 × 2 = 200ms 주기. // PAIX/Ajin 드라이버 모두 비트 1개를 읽을 때마다 DI(DO) 배열 전체를 조회하므로 // (AlarmManager 가 Door 판정에 80ms 캐시를 둔 것과 같은 이유) 매 틱마다 읽지 않는다. private const int EMISSION_REFRESH_TICKS = 2; private string _EmissionText = "IO 미연결"; public string EmissionText { get => _EmissionText; set => SetProperty(ref _EmissionText, value); } private string _EmissionColor = COLOR_EMISSION_NA; public string EmissionColor { get => _EmissionColor; set => SetProperty(ref _EmissionColor, value); } // 두 신호를 각각 확인할 수 있게 램프 안에 같이 찍는다. DI 거동 현장 대조용. private string _EmissionSignalText = "DI:- / DO:-"; public string EmissionSignalText { get => _EmissionSignalText; set => SetProperty(ref _EmissionSignalText, value); } /// IO Device 지연 조회. 화면 생성 시점에 Device 등록이 끝나지 않았을 수 있다. private IODevice ResolveIODevice() { if (_IODevice == null) { try { _IODevice = _Aurora.DeviceManager[CommonDeviceIds.IODevice] as IODevice; } catch (Exception ex) { Log.E($"[LaserEmission] IODevice 조회 실패: {ex.Message}"); } } return _IODevice; } private void SetEmissionNotAvailable(string text) { EmissionText = text; EmissionColor = COLOR_EMISSION_NA; EmissionSignalText = "DI:- / DO:-"; } /// 200ms 주기로 출사중 램프를 갱신한다. private void RefreshEmissionIndicator() { if (++_EmissionTick < EMISSION_REFRESH_TICKS) return; _EmissionTick = 0; bool di, doRemote; try { var io = ResolveIODevice(); if (io == null || !io.IsConnected()) { SetEmissionNotAvailable("IO 미연결"); return; } di = io.ReadDI((int)ePrismaticDIAddress.Laser_EMISSION_ON); doRemote = io.ReadDO((int)ePrismaticDOAddress.Laser_REMOTE); } catch (Exception ex) { Log.E($"[LaserEmission] IO 읽기 실패: {ex.Message}"); SetEmissionNotAvailable("IO 읽기 실패"); return; } EmissionSignalText = $"DI:{(di ? "ON" : "OFF")} / DO:{(doRemote ? "ON" : "OFF")}"; if (di) { EmissionText = "레이저 출사중"; EmissionColor = COLOR_EMISSION_ON; } else if (doRemote) { EmissionText = "출사 대기 (Remote ON)"; EmissionColor = COLOR_EMISSION_WAIT; } else { EmissionText = "OFF"; EmissionColor = COLOR_EMISSION_OFF; } } /// /// 출사 조건 스냅샷을 로그 1줄로 남긴다. 출사가 안 될 때 무엇이 걸렸는지 추적용. /// 판정만 하고 아무것도 차단하지 않는다. 예외가 나도 출사 동작에 영향이 없도록 전부 감싼다. /// private void LogLaserCondition(string tag) { try { // Door 는 AlarmManager 의 80ms 캐시를 그대로 쓴다(추가 IO 읽기 없음). bool doorOpen = AlarmManager.IsDoorOpen("ScannerLaserControl"); bool doorPlc = AlarmManager.IsPlcDoorOpen(); bool doorIo = AlarmManager.IsIoDoorOpen(); bool emo = AlarmManager.IsPlcEmoAlarm(); string alarmText = "확인불가"; try { var codes = _Aurora.AlarmService?.GetCurrentAlarmCodes(); alarmText = (codes == null || codes.Count == 0) ? "없음" : $"{codes.Count}건({string.Join(",", codes.Take(5))})"; } catch (Exception ex) { alarmText = $"조회실패({ex.Message})"; } string diReady = "-", diAlarm = "-", doRemote = "-"; var io = ResolveIODevice(); if (io != null && io.IsConnected()) { diReady = io.ReadDI((int)ePrismaticDIAddress.Laser_READY) ? "T" : "F"; // DI Laser_ALARM 은 Active-Low — 정상 High, 알람 발생 시 Low diAlarm = io.ReadDI((int)ePrismaticDIAddress.Laser_ALARM) ? "정상" : "알람"; doRemote = io.ReadDO((int)ePrismaticDOAddress.Laser_REMOTE) ? "T" : "F"; } bool busy = false; try { busy = _Scanner != null && _Scanner.GetBusyStatus(); } catch { } string modelName = "-"; try { modelName = EquipmentSpecificModelUtil.GetCurrentRecipeEquipmentSpecificModel().name; } catch { } Log.I($"[LaserCond][{tag}] Door={(doorOpen ? "OPEN" : "CLOSE")}(PLC={doorPlc},IO={doorIo}) " + $"EMO={emo} Alarm={alarmText} " + $"DI8_READY={diReady} DI9_ALARM={diAlarm} DO9_REMOTE={doRemote} " + $"NoLaser={IsNoLaser} SpotTime={IsUseLaserAutoOffChecked} Busy={busy} " + $"Model={modelName} SeqTeaching={_SeqTeachingRun?.RunState}"); } catch (Exception ex) { Log.E($"[LaserCond][{tag}] 조건 로그 실패: {ex.Message}"); } } #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 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 drawObjects = new List(); 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}")); // } //} } }