2026-06-05 13:44:39 +09:00

506 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, bool isSimul)
{
DeviceId = deviceId;
Type = DeviceType.PLC;
IsSimul = isSimul;
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) {
BitInputWordSize += 1;
}
BitOutputWordSize = BitOutputBlock.Count / _wordSize;
if ((BitOutputBlock.Count % _wordSize) != 0) {
BitOutputWordSize += 1;
}
MBitInputWordSize = MBitInputBlock.Count / _wordSize;
if ((MBitInputBlock.Count % _wordSize) != 0) {
MBitInputWordSize += 1;
}
MBitOutputWordSize = MBitOutputBlock.Count / _wordSize;
if ((MBitOutputBlock.Count % _wordSize) != 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();
AllBitClear(ePLCDevice.M);
// 업데이트 스레드가 시작되기 전에 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();
AllBitClear(ePLCDevice.M);
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
}
}