using Aurora.Api; using Aurora; using AuroraDeviceCommon.Utils; using AuroraPLC; using Hermes; using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; using System.Threading.Tasks; using AuroraMelsecNet; using AuroraMXComponent; using System.Reactive.Linq; using System.Runtime.Remoting.Contexts; using WeldingCommon.Utils; using System.Threading; using WeldingCommon.Alarm; namespace WeldingCommon.Devices { // 메모리 맵핑을 abstract로 강제화하는 것 다시 추가(5:06) 다시제거(08:30) public abstract partial class PLCController : PLCBase { // Log 사용 방법 static Logger Log = new Logger("PLCController"); //static Logger EventLog = new Logger(eMsg_TYPE.PLC.ToString(), "Event"); //MonitorPlcInterfaceMap 스레드는 타이머로 구성 public MicroTimer _monitorPlcInterfaceMap = new MicroTimer(); DateTime Livetimer; bool beforeLiveStatus = false; bool updateLiveWait = false; #region 생성자 IAuroraEnvironment _AuroraEnv; public PLCController(string deviceId, ePLCNetType plcNetType, bool isSimul, PLCMMapFileType fileType, IAuroraEnvironment aurora) : base(deviceId, isSimul) { _AuroraEnv = aurora; // PLC Network 생성 // PLCNet 의 생명주기 관리 NetworkType = plcNetType; try { if (plcNetType == ePLCNetType.MelsecNet) { // MelsecNet #warning MXComponent 사용 시 AuroraMelsecNet Reference 삭제 및 new MelsecNet 코드 주석처리 PLCNet = new MelsecNet(); } else if (plcNetType == ePLCNetType.MXComponent) { // MxComponent #warning MelsecNet 사용 시 AuroraMXComponent Reference 삭제 및 new MXComponent 코드 주석처리 PLCNet = new MXComponent(); // 주석 해제 필요 } else { // 통신 프로토콜 추가시, 여기에 추가... // 에러 Log.E("Unsupported network type"); } // 메모리맵 파싱 관련 mmapFileType = fileType; //생성자에서 메모리 맵핑이 완료되야됨. LoadMemoryMapFromFiles(); // PLC Motions 의 시뮬레이션은 추후 재검토 필요 if (PLCNet is null) { Log.E("PLC Network (MXComponent or MelsecNet) is not existed"); } #region PLC 모션축 세팅 Observable.Merge( aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_DEVICE_ACTIVATED), aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_DEVICE_DEACTIVATED)) .Select(msg => msg.Param as string) .Where(x => x == deviceId) .Subscribe(x => OnDeviceActiveChanged() ); #endregion } catch (Exception ex) { Log.FuncE(ex.Message); PLCNet = null; } // Event Publisher 확인(이전 PLC에 있는 타이머 부분 추가) _monitorPlcInterfaceMap.OnTimeout += MonitorPlcInterfaceMap; } public void MonitorPlcInterfaceMap() { // Live Bit Control LiveStatusUpdate(); var isAutoRequest = ReadBit(eCommonBitInput.PLC_AUTO_REQ).data; var isManualRequest = ReadBit(eCommonBitInput.PLC_MANUAL_REQ).data; #region [진단계측 2026-08-13] 모드 3비트 상태 전이 로그 (원복: 이 region 통째로 삭제) // 목적: PLC 가 M70501(AUTO_STATUS)/M70502(AUTO_REQ)/M70503(MANUAL_REQ) 를 실제로 쓰는지 확정. // · 아래 rising edge 분기는 ON 전이만 찍으므로 OFF 전이·상시 ON 여부를 알 수 없다. // · ChangeChecker 는 "MonitoringValue 로 바뀐 순간"만 true 라, ON/OFF 를 키 2개로 나눠 잡는다. // · 변화가 없으면 한 줄도 남지 않는다(100ms 주기여도 로그 폭주 없음). // 주의: 이 블록은 아래 "AUTO/MANUAL 동시 ON → return" 가드보다 앞에 둔다. 동시 ON 구간도 봐야 한다. var isAutoStatus = ReadBit(eCommonBitInput.PLC_AUTO_STATUS).data; if (ChangeChecker.IsChangedBoolMonitoring("DiagAutoStatusOn", isAutoStatus, MonitoringValue: true)) Log.W("[진단계측] M70501 PLC_AUTO_STATUS OFF→ON"); if (ChangeChecker.IsChangedBoolMonitoring("DiagAutoStatusOff", isAutoStatus, MonitoringValue: false)) Log.W("[진단계측] M70501 PLC_AUTO_STATUS ON→OFF"); if (ChangeChecker.IsChangedBoolMonitoring("DiagAutoReqOn", isAutoRequest, MonitoringValue: true)) Log.W("[진단계측] M70502 PLC_AUTO_REQ OFF→ON"); if (ChangeChecker.IsChangedBoolMonitoring("DiagAutoReqOff", isAutoRequest, MonitoringValue: false)) Log.W("[진단계측] M70502 PLC_AUTO_REQ ON→OFF"); if (ChangeChecker.IsChangedBoolMonitoring("DiagManualReqOn", isManualRequest, MonitoringValue: true)) Log.W("[진단계측] M70503 PLC_MANUAL_REQ OFF→ON"); if (ChangeChecker.IsChangedBoolMonitoring("DiagManualReqOff", isManualRequest, MonitoringValue: false)) Log.W("[진단계측] M70503 PLC_MANUAL_REQ ON→OFF"); #endregion if (isAutoRequest && isManualRequest) { Log.I("PLC 에서 Auto Run Start 와 Manual 전환 신호가 동시에 들어왔습니다. 신호를 무시합니다."); return; } #region PLC 신호중에 Command 신호 들어온 것에 대한 처리. rising edge 인 경우에만 반응하도록 코딩할 것 if (ChangeChecker.IsChangedBoolMonitoring("MainSeqStart", isAutoRequest, MonitoringValue: true)) { // [진단계측 2026-08-13] PLC_AUTO_REQ / PLC_MANUAL_REQ 실사용 여부 확인용. 원복: 이 Log 두 줄만 삭제. // 배경: 2026-08-12 현장 로그에 이 두 비트를 직접 찍는 흔적이 없어, PLC 가 REQ 를 주는지 // PC 가 자체 버튼으로 움직였는지 사후 판별이 불가능했다. Log.W("[진단계측] PLC_AUTO_REQ rising edge → EVENT_AUTO_RUN_START_REQUEST 발행"); EventPublisher.Publish(new EventMessage("PLC", CommonEventIds.EVENT_AUTO_RUN_START_REQUEST)); } //260512 잠시 주석처리 if (ChangeChecker.IsChangedBoolMonitoring("MainSeqStop", isManualRequest, MonitoringValue: true)) { Log.W("[진단계측] PLC_MANUAL_REQ rising edge → EVENT_AUTO_RUN_STOP_REQUEST 발행"); EventPublisher.Publish(new EventMessage("PLC", CommonEventIds.EVENT_AUTO_RUN_STOP_REQUEST)); } var isRecipeUpdate = ReadBit(eCommonBitInput.PLC_RMS_PARAMETER_READ_REQUEST).data; if (ChangeChecker.IsChangedBoolMonitoring("RmsParameterReadReq", isRecipeUpdate, MonitoringValue: true)) { EventPublisher.Publish(new EventMessage("PLC", CommonEventIds.EVENT_RMS_PARAMETER_READ_REQUEST)); } #endregion } private void LiveStatusUpdate() { if (IsSimul) return; // Live 신호 깜빡이기 // PLC 상태 변경 후 특정 시간동안 대기 하다가 값 넘겨주기 if (updateLiveWait) { if (DateTime.Now.Subtract(Livetimer).TotalMilliseconds > 1000) { updateLiveWait = false; WriteBit(eCommonBitOutput.PC_LIVE_STATUS, beforeLiveStatus); } } else { bool isLive = ReadBit(eCommonBitInput.PLC_LIVE_STATUS).data; // PLC 상태가 변한경우 if (isLive != beforeLiveStatus) { Livetimer = DateTime.Now; beforeLiveStatus = isLive; updateLiveWait = true; } // 10초간 PLC 상태가 변하지 않는 경우 TimeoOut 10000 //260512 주석 처리 잠시 if (DateTime.Now.Subtract(Livetimer).TotalMilliseconds > 10000) { AlarmManager.Alarm(AlarmCode.ALARM_DEVICE_PLC_DISCONNECT); //Log.E($"PLC HeartBeat Signal Is Not Working"); } } } public void RegisterMotion() { #region PLC 모션축 세팅 if (HasMotionAxes && _plcMotorList.Count == 0) { // 모션축 등록.... (수정필요) //_plcMotorList = new List(); //var motorList = DeviceIds.GetMotorDeviceIDs(); //foreach (var item in motorList) { // _plcMotorList.Add(_AuroraEnv.DeviceManager[item] as PLCMotorBase); //} //// 별칭 설정 (인덱스는 ACS 랑 차후 검토 필요함...) (Inkjet 은 ACS 축은 없음) //stageX = _plcMotorList[(int)eAxis.Stage_X - (int)eAxis.Stage_X]; //headY = _plcMotorList[(int)eAxis.Head_Y - (int)eAxis.Stage_X]; //headZ = _plcMotorList[(int)eAxis.Head_Z - (int)eAxis.Stage_X]; //stageT = _plcMotorList[(int)eAxis.Stage_T - (int)eAxis.Stage_X]; //// 메모리맵 Config 를 Readonly 로 세팅함..각 모터 override 활성화에서 처리 } #endregion } #region 활성화/비활성화 protected override int DoActivate(IDeviceConfig config) { //============================================== // 로그 조건 세팅 //IsLogModePLCDetail = SolderingUtil.GetLogSetting().LOG_MODE_PLC_DETAIL; IsLogModePLCDetail = false; // MicroTimer 설정 _monitorPlcInterfaceMap.UpdateRateUS = 100 * 1000; #region 260810 기동 오알람(210002) 수정 — 모니터 타이머 기동 위치 [수정 전] // [수정 전] 여기서 타이머를 기동하면 base.DoActivate() 로 PLC 연결이 열리기도 전에 // LiveStatusUpdate() 가 돌아 ALARM_DEVICE_PLC_DISCONNECT(210002) 오알람 발생. //_monitorPlcInterfaceMap.Start(); // // [수정 후] 아래 "[수정 후]" region 으로 이동 (base.DoActivate() 성공 + Livetimer 확보 뒤) #endregion var res = base.DoActivate(config); //Activate할 때, 한번 더 주소 맵핑 필요 (맵핑이 생성자에서 통신 세팅보다 느림) LoadMemoryMapFromFiles(); if (res != E1.OK) { // PLC 통신 실패 return res; } beforeLiveStatus = false; updateLiveWait = false; #region 260810 기동 오알람(210002) 수정 — Livetimer 초기화 + 타이머 기동 [수정 후] // 원인 : Livetimer 가 초기화되지 않아 DateTime.MinValue 인 상태로 LiveStatusUpdate() 가 돌면 // (Now - MinValue) 가 항상 10000ms 를 초과해 210002 가 발생한다. // MicroTimer 의 첫 콜백은 주기(100ms) 뒤가 아니라 Start() 직후(약 1ms)라 100% 재현. // 임계값 10000 을 키우는 방식으로는 해결되지 않는다(MinValue 앞에서는 모두 초과). // 실측 : 2026-08-10 16:12:25.904 로그 // Livetimer=0001-01-01, elapsed=63921975145904ms, IsActivated=False, IsConnected=False // 조치 : 기준시각을 확보한 뒤 모니터 타이머를 기동한다. // 원복 : 이 region 을 삭제하고, 위 "[수정 전]" region 의 Start() 주석을 해제한다. // 상세 : docs/분석_PLC210002_기동오알람_260810.md Livetimer = DateTime.Now; _monitorPlcInterfaceMap.Start(); #endregion return res; } protected override int DoDeactivate() { var res = base.DoDeactivate(); _monitorPlcInterfaceMap.Stop(); // 비동기 통신이 종료되었는지 확인하는 로직으로 교체 if (!WaitUntilUpdateTerminateMonitor(1000, out int elapsedMSForUpdateTerminate)) { // 업데이트 스레드 종료가 아직 안됨 Log.E("Data update is not teminated yet"); return E2.CANCELLED; } if (res != E1.OK) { Log.E("Deactivate is failed"); return res; } return res; } public bool WaitUntilUpdateTerminateMonitor(int timeOutMS, out int elapsedMS) { elapsedMS = timeOutMS; // convert to Max count var delayMS = 10; int maxCount = timeOutMS / delayMS; int i; for (i = 0; i < maxCount; i++) { if (_monitorPlcInterfaceMap.IsRunning) { // 대기 // 짧은 주기로 검사하자 !! Thread.Sleep(delayMS); continue; } else { // Update thread is terminated elapsedMS = i * delayMS; return true; } } // 실패 return false; } private void OnDeviceActiveChanged() { if (IsActivated) { // PLC 연결 성공 // PLCMotion 스레드 관련 // 시뮬 모드이고, PLCMotorList 가 비어있지 않으면 모션 스레드 생성 if (IsSimul && _plcMotorList.Count > 0) { StartPLCMotionThread(); // 스레드 생성 및 시작 } // Mover 활성화(수정필요) //MachineMover.GetInstanceMachineMover()?.Activate(); } else { // Mover 비활성화 //MachineMover.GetInstanceMachineMover()?.Deactivate(); StopPLCMotionThread(); } } #endregion #region 재연결 public override int ReconnectNetwork() { var res = base.ReconnectNetwork(); return res; } #endregion #region 메모리맵 파일 파싱 public void LoadMemoryMapFromFiles() { /* if (!ParsePLCAlarmSettingAddress(AlarmSettingPath, ref _stAlarmList, ref _stSettingList, out ePLCErrCode retCode)) { if (retCode == ePLCErrCode.MemMapPaser_SETTINGALARM_LOAD_FAIL) { _alarmService.SetAlarm(AlarmCode.ALARM_PLC_EXCEL_SETTINGALARM_LOAD_FAIL); } Log.E("SettingAlarm MMap parsing is failed"); } */ // PLC 메모리 주소를 매핑함 InitPLCAddress(); // 정상여부 체크... if (BitInputs.Count == 0) { Log.W("No. of Bit Input MMap is zero"); } if (BitOutputs.Count == 0) { Log.W("No. of Bit Output MMap is zero"); } if (WordInputs.Count == 0) { Log.W("No. of Word Input MMap is zero"); } if (WordOutputs.Count == 0) { Log.W("No. of Word Output MMap is zero"); } } #endregion // 추후에 폴더 생성하고 옮기기 public enum eAxis { None = -1, //==================================== // Motor Example Stage_X = 0, Head_Y, Head_Z, Stage_T, AXIS_MAX_NUM }; } } #endregion