514 lines
19 KiB
C#
514 lines
19 KiB
C#
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<bool>();
|
|
BitOutputBlock = new IOBlock<bool>();
|
|
MBitInputBlock = new IOBlock<bool>();
|
|
MBitOutputBlock = new IOBlock<bool>();
|
|
WordInputBlock = new IOBlock<short>();
|
|
WordOutputBlock = new IOBlock<short>();
|
|
DWordInputBlock = new IOBlock<short>();
|
|
DWordOutputBlock = new IOBlock<short>();
|
|
|
|
// 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
|
|
}
|
|
}
|