using Aurora.Api; using AuroraDeviceCommon.Utils; using Hermes; using System.Diagnostics; using System.Threading; using Aurora.Api.Devices; using Aurora; using AuroraDeviceCommon.Views; namespace AuroraPLC { public abstract partial class PLCBase : Device //, IDeviceInitialize { Logger Log = new Logger("PLCBase"); public bool IsLogModePLCDetail { get; protected set; } = false; // Aurora 관련 //protected IAlarmService _alarmService; //public IAlarmService AlarmService => _alarmService; // PLC 통신용 네트워크 public PLCNetwork PLCNet; public IConnConfig _connConfig; public bool IsConnected { get; protected set; } = false; public bool NeedReconnect { get; protected set; } = false; // 네트워크 타입 확인용 public ePLCNetType NetworkType { get; protected set; } // 최소 딜레이 protected const int minDelayMS = 4; // 현재 PLC (MxComponent) 업데이트 주기: 50 [ms] protected const int timeOut1000MS = 1000; // config 의 업데이트 주기 저장 For 구동 중 사용 목적 public int CurConfigUpdateRateMS { get; protected set; } #region 필드 정의 public enum eLangType { Unknown = -1, Korean = 0, English = 1, } // Aurora 초기 언어 설정을 어떻게 가져오지..?? //static public eLangType Language = eLangType.Korean; static public eLangType Language = eLangType.English; #endregion // Update Timer 동작 확인용 public stPLCUpdatorInfo PLCUpdatorInfo; // Create a new MicroTimer for data update thread public MicroTimer _dataUpdateTimer = new MicroTimer(); #region 생성자 public PLCBase(string deviceId) { DeviceId = deviceId; Type = DeviceType.PLC; CommType = CommunicationType.IPAndStationNo; //--------------------------------------------- // 관련 필드 초기화 IsConnected = false; PLCUpdatorInfo = new stPLCUpdatorInfo(); // PLC 언어 업데이트 // 비고: 고정부에서 최초 한국어로 언어 변경 이벤트 발생 후, EquipmentSetup 로딩이 진행됨 // 부팅 시의 언어는 유동적일 수 있음 (기본값 or 한국어) var lang = ApplicationExt.CurLang; UpdatePLCLanguage(ref lang); // 업데이트 타이머 등록 _dataUpdateTimer.OnTimeout += DataUpdator; //======================================= } #endregion #region 활성화/비활성화 protected override int DoActivate(IDeviceConfig config) { // 예외 처리 if (PLCNet is null) { return E2.UNAVAILABLE; } _connConfig = config.ConnConfig; if (NetworkType == ePLCNetType.MelsecNet) { //_connConfig = config.ConnConfig as PLCMelsecNetConnConfig; var l_connConfig = _connConfig as PLCMelsecNetConnConfig; // Melsec 메뉴얼에는 mode 는 더미로 기본 -1 이라고 되어 있음 PLCNet.SetInitParams(l_connConfig.ChannelNumber, -1, l_connConfig.StationNumber); CurConfigUpdateRateMS = l_connConfig.UpdateRateMS; } else if (NetworkType == ePLCNetType.MXComponent) { //_connConfig = config.ConnConfig as PLCMXComponentConnConfig; var l_connConfig = _connConfig as PLCMXComponentConnConfig; PLCNet.SetInitParams(l_connConfig.StationNumber); CurConfigUpdateRateMS = l_connConfig.UpdateRateMS; } if (!Open()) { Log.E("PLC Open was failed"); return E1.ARGUMENT; } Log.I("PLC Open success"); IsConnected = true; //================================================ //_curConfigUpdateRateMS = _connConfig.UpdateRateMS; // update 타이머 세팅 _dataUpdateTimer.UpdateRateUS = CurConfigUpdateRateMS * 1000; //_dataUpdateTimer.FeedbackPerMS = 0; // No use //========================================= // Bit & Word 메모리 버퍼 생성 if (BitInputBlock == null || BitOutputBlock == null || MBitInputBlock == null || MBitOutputBlock == null || WordInputBlock == null || WordOutputBlock == null || DWordInputBlock == null || DWordOutputBlock == null) { BitInputBlock = new IOBlock(); BitOutputBlock = new IOBlock(); MBitInputBlock = new IOBlock(); MBitOutputBlock = new IOBlock(); WordInputBlock = new IOBlock(); WordOutputBlock = new IOBlock(); DWordInputBlock = new IOBlock(); DWordOutputBlock = new IOBlock(); // Bit 메모리 버퍼 초기화 InitBitBlock(BitInputs, BitInputBlock); InitBitBlock(BitOutputs, BitOutputBlock); InitBitBlock(MBitInputs, MBitInputBlock, ePLCDevice.M); InitBitBlock(MBitOutputs, MBitOutputBlock, ePLCDevice.M); InitWordBlock(WordInputs, WordInputBlock); InitWordBlock(WordOutputs, WordOutputBlock); InitWordBlock(DWordInputs, DWordInputBlock, ePLCDevice.D); InitWordBlock(DWordOutputs, DWordOutputBlock, ePLCDevice.D); } // B/W 에 대한 송수신 size 를 워드단위로 계산 (Aurora 실행후에는 불변) BitInputWordSize = BitInputBlock.Count / _wordSize; if ((BitInputBlock.Count % _wordSize) != 0) { if (BitInputBlock.Count != 0) { BitInputWordSize += 1; } } BitOutputWordSize = BitOutputBlock.Count / _wordSize; if ((BitOutputBlock.Count % _wordSize) != 0) { if (BitOutputBlock.Count != 0) { BitOutputWordSize += 1; } } MBitInputWordSize = MBitInputBlock.Count / _wordSize; if ((MBitInputBlock.Count % _wordSize) != 0) { if(MBitInputBlock.Count != 0) { MBitInputWordSize += 1; } } MBitOutputWordSize = MBitOutputBlock.Count / _wordSize; if ((MBitOutputBlock.Count % _wordSize) != 0) { if(MBitOutputBlock.Count != 0) { MBitOutputWordSize += 1; } } WordInputWordSize = WordInputBlock.Count; WordOutputWordSize = WordOutputBlock.Count; DWordInputWordSize = DWordInputBlock.Count; DWordOutputWordSize = DWordOutputBlock.Count; // 기타: B 장치 Bit 개수 BitInputSize = BitInputBlock.Count; BitOutputSize = BitOutputBlock.Count; MBitInputSize = MBitInputBlock.Count; MBitOutputSize = MBitOutputBlock.Count; // 솔더링의 경우, 통신이 연결되면, 최초 한번 출력 Bit를 모두 0 이 되도록 PLC 에 송신함... // C# 버전이 낮아서, 해당 API 사용 불가 //var updateTimer = new PeriodicTimer(TimeSpan.FromMilliseconds(100)); //--------------------------------------- // Data processing //swDataUpdate.Start(); //--------- Updator Info --------------------- // reset PLCUpdatorInfo = new stPLCUpdatorInfo(); // PLC 통신 초기시, PLC 출력값을 PC 출력버퍼에 복사(동기화)한다. if (IsSimul == false) { // Real 모드 // Read PLC Bit/Word Outputs ReadBitOutputFromPLC(); ReadMBitOutputFromPLC(); ReadWordOutputFromPLC(); ReadDWordOutputFromPLC(); } else { // Simul 모드 // PC 의 Ouput B/W 버퍼를 PLC 와 동기화해야 하는데, 시뮬은 불필요 ?? // TODO } // 모든 출력비트 reset AllBitClear(); // 업데이트 스레드가 시작되기 전에 PLC reset 수행 ManualDataUpdate(); // Start the timer _dataUpdateTimer.Start(); //======================================== #region PLC Motion 관련 처리 foreach (var item in _plcMotorList) { item.Activate(); } #endregion return E1.OK; } public bool Open() { if (IsSimul == false) { // Real mode Log.I("Try to connect to the Real PLC"); return PLCNet.OpenToPLC(); } else { // Simul mode Log.I("Try to connect to the Sim. PLC"); return true; } } protected override int DoDeactivate() { #region PLC Motion 관련 처리 foreach (var item in _plcMotorList) { item.Deactivate(); } #endregion if (IsSimul == false) { // Real // PLC를 비활성화하기 위해서는 먼저 안정적으로 통신을 해제해야 한다. // 1. 업데이트 스레드 종료 // 2. 통신 해제 // 3. 비활성화 // Stop the data update timer _dataUpdateTimer.Stop(); // 비동기 통신이 종료되었는지 확인하는 로직으로 교체 (개선 검토..???) if (!WaitUntilUpdateTerminate(1000, out int elapsedMSForUpdateTerminate)) { // 업데이트 스레드 종료가 아직 안됨 Log.E("Data update is not teminated yet"); return E2.CANCELLED; } Log.I($"Data update is terminated: {elapsedMSForUpdateTerminate} [ms] elapsed"); // 풍산과 동일하게 Bit 입출력을 clear 하고, PLC 연결을 종료하자. : 2개 메서드를 합쳐도 될 것 같은데... 추후 검토.. AllBitClear(); ManualDataUpdate(); if (PLCNet.CloseToPLC() != true) { return E1.NETWORK; } } else { // 시뮬 모드: 데이터 업데이트 스레드 종료 _dataUpdateTimer.Stop(); // 비동기 통신이 종료되었는지 확인하는 로직으로 교체 if (!WaitUntilUpdateTerminate(1000, out int elapsedMSForUpdateTerminate)) { // 업데이트 스레드 종료가 아직 안됨 Log.E("Data update is not teminated yet"); return E2.CANCELLED; } Log.I($"Data update is terminated: {elapsedMSForUpdateTerminate} [ms] elapsed"); } IsConnected = false; // PLC 구동 상태 정보를 로그로 남긴다. Log.I("PLC Work History"); Log.I($"No. Reconnection: {PLCUpdatorInfo.NumberOfReconnection}"); Log.I($"Update Fails: RB: {PLCUpdatorInfo.NumberOfRBFail}, WB: {PLCUpdatorInfo.NumberOfWBFail}, RW: {PLCUpdatorInfo.NumberOfRWFail}, WW: {PLCUpdatorInfo.NumberOfWWFail}"); return E1.OK; } #endregion #region 재연결 public virtual int ReconnectNetwork() { if (!IsSimul) { // 리얼 모드 // 더블체크 IsConnected = false; // InformPanel 상태 컬러 제어용.. IsCommError = true; // 네트워크 연결에 문제 발생...!!! // 풍산과 동일하게 모든 Bit 입력을 Clear AllBitClear(isOutput: false); // MX Component 의 경우, Open 과 Close 가 순차적으로 호출되어야 하는 것 같다.. // Close 만 계속 호출하면, 0x80201201 (Not yet open error) 가 발생함.. // 재연결에서는 Close/Open 에러에 신경쓰지 말고 무조건 Close -> Open 을 호출하자 // - error code 를 받아야 할지 고민임.. // Close 호출 PLCNet.CloseToPLC(); // Open 호출 if (!PLCNet.OpenToPLC()) { Log.E("ReconnectNetwork: Open was failed"); return E1.NETWORK; } ++PLCUpdatorInfo.NumberOfReconnection; // 재연결 성공 횟수 // 모든 출력비트 reset : 풍산 방식과 동일한 조건 유지... AllBitClear(); ManualDataUpdate(); Thread.Sleep(CurConfigUpdateRateMS); Log.I("PLC Open success"); IsConnected = true; // 업데이트 스레드 재개 // InformPanel 상태 컬러 제어용.. IsCommError = false; // 네트워크 연결 정상...!!! } return E1.OK; } #endregion #region 특수 Case public virtual bool CheckProcessAutoMode() { return false; } #endregion #region 유틸리티 //private string _lang; public void UpdatePLCLanguage(ref string lang) { // 필요할때만 호출할 것이므로, 항상 업데이트 필요 //if (lang.Equals(_lang)) return; switch (lang) { case "ko-KR": { Language = eLangType.Korean; break; } case "en-US": { Language = eLangType.English; break; } } //_lang = lang; } public eLangType GetLangTypeFrom(string lang) { UpdatePLCLanguage(ref lang); switch (lang) { case "ko-KR": { return eLangType.Korean; } case "en-US": { return eLangType.English; } default: { return eLangType.Unknown; } } } // 오직 PLC가 활성화한 시점에서만 사용됨 public bool WaitUntilFirstRead(int timeOutMS, out int elapsedMS) { // 사용빈도가 낮기때문에, 스탑워치를 써보자... var _sleepTimeMS = CurConfigUpdateRateMS / 10; if (_sleepTimeMS < 10) { _sleepTimeMS = 10; } Stopwatch _sw = new Stopwatch(); _sw.Start(); do { if (PLCUpdatorInfo.NumberOfUpdateCycle == 0) { // 대기 Thread.Sleep(_sleepTimeMS); continue; } else { // The first read is executed elapsedMS = (int)_sw.ElapsedMilliseconds; _sw.Stop(); return true; } } while (_sw.ElapsedMilliseconds < timeOutMS); /* elapsedMS = timeOutMS; // convert to Max count int maxCount = timeOutMS / _config.UpdateRateMS; int i; for (i = 0; i < maxCount; i++) { if (NumberOfUpdateCycle == 0) { // 대기 Thread.Sleep(_config.UpdateRateMS); continue; } else { // The first read is executed elapsedMS = i * _config.UpdateRateMS; return true; } } */ // 실패 elapsedMS = (int)_sw.ElapsedMilliseconds; _sw.Stop(); return false; } public bool WaitUntilUpdateTerminate(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 (_dataUpdateTimer.IsRunning) { // 대기 // 짧은 주기로 검사하자 !! Thread.Sleep(delayMS); continue; } else { // Update thread is terminated elapsedMS = i * delayMS; return true; } } // 실패 return false; } public bool WaitUntilDeactivated(int timeOutMS, out int elapsedMS) { elapsedMS = timeOutMS; // convert to Max count var delayMS = 50; int maxCount = timeOutMS / delayMS; int i; for (i = 0; i < maxCount; i++) { if (IsActivated) { // 대기 Thread.Sleep(delayMS); continue; } else { // Update thread is terminated elapsedMS = i * delayMS; return true; } } // 실패 return false; } public bool WaitAtLeastOneCycle(int timeOutMS, out int elapsedMS) { elapsedMS = timeOutMS; // convert to Max count int maxCount = timeOutMS / CurConfigUpdateRateMS; // latch var curUpdateCycle = PLCUpdatorInfo.NumberOfUpdateCycle; int i; for (i = 0; i < maxCount; i++) { if (curUpdateCycle <= PLCUpdatorInfo.NumberOfUpdateCycle) { // 대기 Thread.Sleep(CurConfigUpdateRateMS); continue; } else { // At least 1 cycle is executed elapsedMS = i * CurConfigUpdateRateMS; return true; } } // 실패 return false; } #endregion #region IDeviceInitialize 인터페이스 //// for test //static bool bIsInitialized = false; //public int Initialize() //{ // // Device Initialize pop-up (UCDeviceActivation)을 위한 샘플 코드 // // "PLC Initialize" 에 필요한 작업이 있다면 여기에 코드 작성 // return IsActivated ? E1.OK : E1.GENERIC; //} //public bool IsInitialized() //{ // // Device Initialize pop-up (UCDeviceActivation)을 위한 샘플 코드 // // "PLC Initialized" 상태로 체크할 조건이 있다면 여기에 코드 작성 // // for test // //if (IsActivated) { // // bIsInitialized = !bIsInitialized; // //} else { // // bIsInitialized = false; // //} // bIsInitialized = IsActivated; // return bIsInitialized; //} #endregion } }