using Aurora; 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; using System.Linq; using System.Reactive.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using WeldingCommon.Devices; using WeldingCommon.Sequences; using static WeldingCommon.Devices.PLCController; namespace WeldingCommon.Alarm { public class AlarmManager { private static readonly Logger AlarmLog = new Logger("ALARM"); protected static AlarmManager _Instance; protected static IAuroraEnvironment _AuroraEnv; public readonly IAlarmService _AlarmService; private readonly IDeviceManager _DeviceManager; private static PLCController _Plc; public AlarmManager(IAuroraEnvironment aurora) { _AuroraEnv = aurora; _AlarmService = aurora.AlarmService; _DeviceManager = aurora.DeviceManager; _Plc = _DeviceManager.GetDevice(CommonDeviceIds.PLCDevice) as PLCController; // Reset Alarm Event Subscribe > Clear All Device Alarms // TODO:: Check when event below generated. If event does not generated, remove. aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_ALARM_RESET_ALL_ALARM) .Select(msg => msg.Param) .Subscribe(ClearAllAlarm); aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_ALARM_RESET_ALARM) .Select(msg => msg.Param) .Subscribe(ClearAlarm); aurora.EventObservable.AsObservable(CommonEventIds.EVENT_RESET_ALL_ALARMS_REQUEST) .Select(msg => msg.Param) .Subscribe(ClearAllAlarmRequest); } public static void Init(IAuroraEnvironment aurora) { if (_Instance == null) { _Instance = new AlarmManager(aurora); } } private void ClearAlarm(object obj) { //각 Alarm별 clear 처리를 따로 해줘야 할까 ? } private void ClearAllAlarm(object mAlarmList) { if (!(mAlarmList is List alarmList)) { return; } //_Plc?.WriteBit(_Plc.LightAlarmStatusAddr, false); //_Plc?.WriteBit(_Plc.HeavyAlarmStatusAddr, false); } private void ClearAllAlarmRequest(object obj) { Application.Current.Dispatcher.InvokeAsync(() => _Instance._AlarmService.ClearAllAlarm()); //_Plc?.WriteBit(_Plc.LightAlarmStatusAddr, false); //_Plc?.WriteBit(_Plc.HeavyAlarmStatusAddr, false); } public static bool Alarm(int code, bool isLog = true, bool useReport = true) { // 모든 Alarm 은 띄워 줌 if (!_Instance._AlarmService.GetCurrentAlarmCodes().Contains(code)) { _Instance._AlarmService.SetAlarm(code, useReport); AlarmDescription alarm = _Instance._AlarmService.GetAlarmDescription(code); if (isLog) { if (alarm.Level == AlarmLevel.H) { AlarmLog.E($"Code : {code} , Description : {alarm.Name}"); } else { AlarmLog.W($"Code : {code} , Description : {alarm.Name}"); } } ////PLC의 중알람의 경우 PC에서 Dialog만 발생하고 Alarm 중알람 Word를 써주지 않는다 //if (alarm.Level == AlarmLevel.H && alarm.Unit == "PLC") { // return true; //} //if (alarm.Level == AlarmLevel.H) { // _Plc?.WriteBit(_Plc.HeavyAlarmStatusAddr, true); //} else { // _Plc?.WriteBit(_Plc.LightAlarmStatusAddr, true); //} return true; } return false; } public static bool ChangeAlarmDescription(int code, string description, bool isLog = true, bool useReport = true) { // 중복 알람은 로그로 남기지 않도록 하는 코드 if (IsOccuredAlarm(code)) return true; AlarmDescription alarm = _Instance._AlarmService.GetAlarmDescription(code); alarm.Description = description; // 모든 Alarm 은 띄워 줌 if (!_Instance._AlarmService.GetCurrentAlarmCodes().Contains(code)) { _Instance._AlarmService.SetAlarm(code, useReport); if (isLog) { if (alarm.Level == AlarmLevel.H) { AlarmLog.E($"Code : {code} , Description : {alarm.Name}"); } else { AlarmLog.W($"Code : {code} , Description : {alarm.Name}"); } } ////PLC의 중알람의 경우 PC에서 Dialog만 발생하고 Alarm 중알람 Word를 써주지 않는다 //if (alarm.Level == AlarmLevel.H && alarm.Unit == "PLC") { // return true; //} //if (alarm.Level == AlarmLevel.H) { // _Plc?.WriteBit(_Plc.HeavyAlarmStatusAddr, true); //} else { // _Plc?.WriteBit(_Plc.LightAlarmStatusAddr, true); //} return true; } return false; } public static void StopCommonSequenceByAlarm(int code) { AlarmDescription alarm = _Instance._AlarmService.GetAlarmDescription(code); // 바로 Sequence 를 정지해야 하는 경우 발생할 수 있음. // 불필요 할까?? Device 의 상태에 따라서 멈춰야 하는 Sequence 가 다를 수도 있다. // 만약에 Sequence 가 종료되었을 때, pass, fail 여부에 따라 다를 지도. if (alarm.Level == AlarmLevel.H) { StopSequence(CommonSequenceIds.SeqMarkGrid); StopSequence(CommonSequenceIds.SeqScannerCalibration); StopSequence(CommonSequenceIds.SeqReceiveCellInfo); StopSequence(CommonSequenceIds.SeqReportCellResult); StopSequence(CommonSequenceIds.SeqScoutFlyingVision); StopSequence(CommonSequenceIds.SeqScoutNormal); StopSequence(CommonSequenceIds.SeqUploadWeldingList); StopSequence(CommonSequenceIds.SeqTeachingRun); } } protected static void StopSequence(string sequenceId) { if (_AuroraEnv.SequenceEntry[sequenceId].RunState == SequenceRunState.Running) { _AuroraEnv.SequenceEntry[sequenceId].EmergencyStop(); AlarmLog.I($"{_AuroraEnv.SequenceEntry[sequenceId]} EmergencyStop"); } } // 공통부. 현재 발생한 중알람 여부 조회 public static bool HasAlarmLevelHigh() { try { if (ApplicationExt.IsSimulation) return false; var alarmList = _Instance._AlarmService.GetCurrentAlarmCodes(); if (alarmList.Count > 0) { foreach (var item in alarmList) { if (IsAlarmLevelHigh(item)) { return true; } } } return false; } catch (Exception ex) { AlarmLog.E($"{ex.Message} Exception Accured"); return false; } } public static bool IsAlarmLevelHigh(int item) { var desc = _Instance._AlarmService.GetAlarmDescription(item); if (desc != null) { if (desc.Level == AlarmLevel.H) { return true; } } else { // Alarm Description을 알 수 없는 경우 : 위험할 수 있으니 일단 H로 취급 return true; } return false; } public static bool HasAlarmLevelLow() { var alarmList = _Instance._AlarmService.GetCurrentAlarmCodes(); if (alarmList.Count > 0) { foreach (var item in alarmList) { if (IsAlarmLevelLow(item)) { return true; } } } return false; } public static bool IsAlarmLevelLow(int item) { var desc = _Instance._AlarmService.GetAlarmDescription(item); if (desc != null) { if (desc.Level == AlarmLevel.L) { return true; } } else { // Alarm Description을 알 수 없는 경우 : 위험할 수 있으니 일단 H로 취급 return true; } return false; } public static bool IsOccuredAlarm(int AlarmCode) { var CurAlarmList = _Instance._AlarmService.GetCurrentAlarmCodes(); if (CurAlarmList.FirstOrDefault(x => x == AlarmCode) != 0) return true; return false; } public static bool IsPlcEmoAlarm() { if (_Plc == null) return false; else return _Plc.ReadBit(eCommonBitInput.PLC_EMO_STATUS).data; } public static bool IsPlcDoorOpen() { // [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 _DoorLogState = new Dictionary(); 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; } } /// /// IO DI 접점(DoorLockOpen1~4) 기준 Door Open 여부. /// IO 미연결/미활성이면 판정 불가로 보고 false 를 반환한다. /// public static bool IsIoDoorOpen() { RefreshDoorCache(); return _DoorIoOpenCache; } /// /// PLC 비트와 IO DI 접점을 OR 하여 Door Open 여부를 판정한다. /// caller 는 로그 식별용 호출부 이름. /// 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; } /// 차단 로그에 붙일 Door 신호 상세 문자열. 캐시값을 쓰므로 추가 IO 읽기가 없다. 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 } }