NohDoyeon241104 3b8e64b9be feat: 레이저 출사중 표시 및 출사조건 로그 (ISSUE-260904-01)
메뉴얼/레시피 화면에 DI Laser_EMISSION_ON(10) 기반 출사중 램프 추가. 260904 현장 실측으로 DI 10 이 DO 9 의 에코가 아닌 실제 발진 피드백임을 확증하여 LaserIOControl 주석도 정정. 출사조건 스냅샷은 [LaserCond] 로그로 4개 시점(ShotOn/Heartbeat/ShotOff/DoorForceOff)에 기록. 표시·기록 전용이며 출사 차단 로직은 변경 없음.
2026-09-04 14:13:13 +09:00

851 lines
41 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 어떤 인터락도 없었다.
/// <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 을 다시 누르십시오.");
// [출사 표시 2026-09-04] 강제 OFF 가 실제로 먹었는지 확인용.
// DI Laser_EMISSION_ON 이 실제 발진 피드백이므로(260904 현장 실측),
// 이 로그에서 출사가 끊겼는지를 신호로 증명할 수 있다.
LogLaserCondition("DoorForceOff");
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()}");
// [출사 표시 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); }
/// <summary>IO Device 지연 조회. 화면 생성 시점에 Device 등록이 끝나지 않았을 수 있다.</summary>
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:-";
}
/// <summary>200ms 주기로 출사중 램프를 갱신한다.</summary>
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;
}
}
/// <summary>
/// 출사 조건 스냅샷을 로그 1줄로 남긴다. 출사가 안 될 때 무엇이 걸렸는지 추적용.
/// 판정만 하고 아무것도 차단하지 않는다. 예외가 나도 출사 동작에 영향이 없도록 전부 감싼다.
/// </summary>
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<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}"));
// }
//}
}
}