2026-03-30 17:09:29 +09:00

1160 lines
52 KiB
C#

using Aurora.Api;
using Hermes;
using System;
using System.Diagnostics;
using System.Threading;
using static AuroraDeviceCommon.Utils.Utils;
using static AuroraPLC.PLCBase;
namespace AuroraPLC
{
// PLCMotorBase 용
public interface IPLCBase
{
(int err, bool data) ReadBit(int addrOffset, ePLCDevice device = ePLCDevice.B, bool isInput = true);
(int err, int data) ReadDWord(int addrOffset, ePLCDevice device = ePLCDevice.W, bool isInput = true);
int WriteBit(int addrOffset, bool onOff, ePLCDevice device = ePLCDevice.B, bool isOutput = true);
int WriteWord(int addrOffset, short shData, ePLCDevice device = ePLCDevice.W, bool isOutput = true);
int WriteDWord(int addrOffset, int iVal, ePLCDevice device = ePLCDevice.W, bool isOutput = true);
bool WaitWriteWordUpdate(ushort latchedUpdatePointer, ulong latchedUpdateCycle, out int elapsedMS, int timeOutMS = 1000);
ushort LatchForUpdatePointer { get; }
ulong LatchForUpdateCycle { get; }
//IAlarmService AlarmService { get; }
}
public abstract partial class PLCBase : IPLCBase
{
#region PLC 통신 메서드 (Aurora 용: 내부 버퍼에 read/write)
//---------------------------------------
// Read
//---------------------------------------
/// <summary>
/// Read PLC Bit
/// ** Read 이므로, 기본값은 isInput = true 임
/// </summary>
/// <param name="addrOffset"></param>
/// <param name="isOutput"></param>
/// <returns>bit value</returns>
public (int err, bool data) ReadBit(int addrOffset, ePLCDevice device = ePLCDevice.B, bool isInput = true)
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return (E2.NOT_READY, false);
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not connected"));
return (E2.NOT_CONNECTED, false);
}
(int err, bool data) res2;
if (isInput) {
if (device == ePLCDevice.M) {
res2 = ReadBit(MBitInputBlock, addrOffset);
} else {
res2 = ReadBit(BitInputBlock, addrOffset);
}
if (res2.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
} else {
if (device == ePLCDevice.M) {
res2 = ReadBit(MBitOutputBlock, addrOffset);
} else {
res2 = ReadBit(BitOutputBlock, addrOffset);
}
if (res2.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
}
return res2;
}
/// <summary>
/// Read PLC Bit
/// ** 위 기본형 ReadBit와 동일한 함수를 return type만 다른 방식으로 제공
/// ** Read 이므로, 기본값은 isInput = true 임
/// </summary>
/// <param name="addrOffset"></param>
/// <param name="data"></param>
/// <param name="isInput"></param>
/// <returns></returns>
public int ReadBit(int addrOffset, out bool data, ePLCDevice device = ePLCDevice.B, bool isInput = true)
{
var result = ReadBit(addrOffset, device, isInput);
data = result.data;
return result.err;
}
/// <summary>
/// Read PLC Word
/// ** Read 이므로, 기본값은 isInput = true 임
/// </summary>
/// <param name="addrOffset"></param>
/// <param name="isInput"></param>
/// <returns></returns>
public (int err, short data) ReadWord(int addrOffset, ePLCDevice device = ePLCDevice.W, bool isInput = true)
{
// 예외처리를 여기에 고정해야함
// 예외일 때 리턴값을 어떻게 하냐가 고민인데, 우선은 false or 0 or null 을 리턴함
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return (E2.NOT_READY, 0);
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not connected"));
return (E2.NOT_CONNECTED, 0);
}
(int err, short data) res2;
if (isInput) {
if (device == ePLCDevice.D) {
res2 = ReadWord(DWordInputBlock, addrOffset);
} else {
res2 = ReadWord(WordInputBlock, addrOffset);
}
if (res2.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
} else {
if (device == ePLCDevice.D) {
res2 = ReadWord(DWordOutputBlock, addrOffset);
} else {
res2 = ReadWord(WordOutputBlock, addrOffset);
}
if (res2.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
}
return res2;
}
/// <summary>
/// Read PLC Word
/// ** Read 이므로, 기본값은 isInput = true 임
/// ** 위 기본형 ReadWord와 동일한 함수를 return type만 다른 방식으로 제공
/// </summary>
/// <param name="addrOffset"></param>
/// <param name="isInput"></param>
/// <returns></returns>
public int ReadWord(int addrOffset, out short data, ePLCDevice device = ePLCDevice.W, bool isInput = true)
{
var result = ReadWord(addrOffset, device, isInput);
data = result.data;
return result.err;
}
/// <summary>
/// Read PLC DWord
/// ** Read 이므로, 기본값은 isInput = true 임
/// </summary>
/// <param name="addrOffset"></param>
/// <param name="isInput"></param>
/// <returns></returns>
public (int err, int data) ReadDWord(int addrOffset, ePLCDevice device = ePLCDevice.W, bool isInput = true)
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return (E2.NOT_READY, 0);
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not connected"));
return (E2.NOT_CONNECTED, 0);
}
// 32bit word: lower, upper 순서
(int err, short[] data) res2;
if (isInput) {
if (device == ePLCDevice.D) {
res2 = ReadWordBlock(DWordInputBlock, addrOffset, 2);
} else {
res2 = ReadWordBlock(WordInputBlock, addrOffset, 2);
}
if (res2.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
} else {
if (device == ePLCDevice.D) {
res2 = ReadWordBlock(DWordOutputBlock, addrOffset, 2);
} else {
res2 = ReadWordBlock(WordOutputBlock, addrOffset, 2);
}
if (res2.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
}
// word = 16 bit, upper word + lower word
// 상위 워드로 이동, lower word 와 비트 or 조합
int outValue = (res2.data[1] << 16) | (res2.data[0] & 0x0000FFFF);
return (res2.err, outValue);
}
/// <summary>
/// Read PLC DWord
/// ** Read 이므로, 기본값은 isInput = true 임
/// ** 위 기본형 ReadDWord와 동일한 함수를 return type만 다른 방식으로 제공
/// </summary>
/// <param name="addrOffset"></param>
/// <param name="isInput"></param>
/// <returns></returns>
public int ReadDWord(int addrOffset, out int data, ePLCDevice device = ePLCDevice.W, bool isInput = true)
{
var result = ReadDWord(addrOffset, device, isInput);
data = result.data;
return result.err;
}
/// <summary>
/// Read PLC DWord (+ Decimal Scale 지정)
/// ** Read 이므로, 기본값은 isInput = true 임
/// </summary>
/// <param name="addrOffset"></param>
/// <param name="scaleDecimalPt"></param>
/// <param name="isInput"></param>
/// <returns></returns>
public (int err, double data) ReadDWord(int addrOffset, double scaleDecimalPt, ePLCDevice device = ePLCDevice.W, bool isInput = true)
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return (E2.NOT_READY, 0);
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not connected"));
return (E2.NOT_CONNECTED, 0);
}
// 메서드 의미
// - 2워드 데이터를 upper/lower 로 32bit 정수형 데이터로 변환
// - scaleDecimalPt 을 이용하여, 실수형으로 변환
// 예외처리
if (Math.Abs(scaleDecimalPt) < 0.001) {
Log.W($"Invalid denominator(zero divide): scaleDecimalPt: {scaleDecimalPt}");
return (E2.INVALID_ARGUMENT, 0);
}
// 32bit word: lower, upper 순서
(int err, short[] data) res2;
if (isInput) {
if (device == ePLCDevice.D) {
res2 = ReadWordBlock(DWordInputBlock, addrOffset, 2);
} else {
res2 = ReadWordBlock(WordInputBlock, addrOffset, 2);
}
if (res2.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
} else {
if (device == ePLCDevice.D) {
res2 = ReadWordBlock(DWordOutputBlock, addrOffset, 2);
} else {
res2 = ReadWordBlock(WordOutputBlock, addrOffset, 2);
}
if (res2.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
}
// word = 16 bit
// 상위 워드로 이동
int dwordValue = (res2.data[1] << 16) | (res2.data[0] & 0x0000FFFF);
double outValue = dwordValue / scaleDecimalPt;
return (res2.err, outValue);
}
/// <summary>
/// Read PLC Word Block (Word를 특정 size만큼 읽기)
/// ** Read 이므로, 기본값은 isInput = true 임
/// </summary>
/// <param name="addrOffset"></param>
/// <param name="scaleDecimalPt"></param>
/// <param name="isInput"></param>
/// <returns></returns>
public (int err, short[] data) ReadWordBlock(int addrOffset, int wordSize, ePLCDevice device = ePLCDevice.W, bool isInput = true)
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return (E2.NOT_READY, null);
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not connected"));
return (E2.NOT_CONNECTED, null);
}
(int err, short[] data) res2;
if (isInput) {
if (device == ePLCDevice.D) {
res2 = ReadWordBlock(DWordInputBlock, addrOffset, wordSize);
} else {
res2 = ReadWordBlock(WordInputBlock, addrOffset, wordSize);
}
if (res2.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
} else {
if (device == ePLCDevice.D) {
res2 = ReadWordBlock(DWordOutputBlock, addrOffset, wordSize);
} else {
res2 = ReadWordBlock(WordOutputBlock, addrOffset, wordSize);
}
if (res2.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
}
return res2;
}
public int ReadWordBlock(int addrOffset, int wordSize, out short[] data, ePLCDevice device = ePLCDevice.W, bool isInput = true)
{
var result = ReadWordBlock(addrOffset, wordSize, device, isInput);
data = result.data;
return result.err;
}
//======================================
//---------------------------------------
// Write
//---------------------------------------
/// <summary>
/// Write PLC Bit
/// ** Write 이므로, 기본값은 isOutput = true 임
/// </summary>
/// <param name="addrOffset"></param>
/// <param name="onOff"></param>
/// <param name="isOutput"></param>
/// <returns></returns>
public int WriteBit(int addrOffset, bool onOff, ePLCDevice device = ePLCDevice.M, bool isOutput = true)
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return E2.NOT_READY;
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.E(WriteLogTag("PLC is not connected"));
return E2.NOT_CONNECTED;
}
int res;
if (isOutput) { // Output
if (device == ePLCDevice.M) {
res = WriteBit(MBitOutputBlock, addrOffset, onOff);
} else {
res = WriteBit(BitOutputBlock, addrOffset, onOff);
}
if (res == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")}"));
return res;
}
} else { // Input
if (IsSimul != true) { // 에러
Log.E(WriteLogTag("Improper Write BitInput operation in real mode"));
return E2.UNAVAILABLE;
}
if (device == ePLCDevice.M) {
res = WriteBit(MBitInputBlock, addrOffset, onOff);
} else {
res = WriteBit(BitInputBlock, addrOffset, onOff);
}
if (res == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")}"));
return res;
}
}
return E1.OK;
}
/// <summary>
/// Write PLC Word
/// ** Write 이므로, 기본값은 isOutput = true 임
/// </summary>
/// <param name="addrOffset"></param>
/// <param name="shData"></param>
/// <param name="isOutput"></param>
/// <returns></returns>
public int WriteWord(int addrOffset, short shData, ePLCDevice device = ePLCDevice.D, bool isOutput = true) // 뮤텍스 필요
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return E2.NOT_READY;
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.E(WriteLogTag("PLC is not connected"));
return E2.NOT_CONNECTED;
}
int res;
if (isOutput) {
if (device == ePLCDevice.D) {
res = WriteWord(DWordOutputBlock, addrOffset, shData);
} else {
res = WriteWord(WordOutputBlock, addrOffset, shData);
}
if (res == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")}"));
return res;
}
} else {
if (IsSimul != true) { // 에러
Log.E(WriteLogTag("Improper Write WordInput operation in real mode"));
return E2.UNAVAILABLE;
}
if (device == ePLCDevice.D) {
res = WriteWord(DWordInputBlock, addrOffset, shData);
} else {
res = WriteWord(WordInputBlock, addrOffset, shData);
}
if (res == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")}"));
return res;
}
}
return E1.OK;
}
/// <summary>
/// Write PLC DWord
/// ** Write 이므로, 기본값은 isOutput = true 임
/// </summary>
/// <param name="addrOffset"></param>
/// <param name="iVal"></param>
/// <param name="isOutput"></param>
public int WriteDWord(int addrOffset, int iVal, ePLCDevice device = ePLCDevice.W, bool isOutput = true)
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return E2.NOT_READY;
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.E(WriteLogTag("PLC is not connected"));
return E2.NOT_CONNECTED;
}
short[] wordOuput = new short[2];
wordOuput[0] = (short)(iVal & 0x0000FFFF); // lower word
wordOuput[1] = (short)((iVal >> 16) & 0x0000FFFF); // upper word (signed 의 경우, MSB 가 1 이면, 왼쪽에 1 이 채워짐)
int res;
if (isOutput) {
if (device == ePLCDevice.D) {
res = WriteWordBlock(DWordOutputBlock, addrOffset, 2, wordOuput);
} else {
res = WriteWordBlock(WordOutputBlock, addrOffset, 2, wordOuput);
}
if (res == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")}"));
return res;
}
} else {
if (device == ePLCDevice.D) {
res = WriteWordBlock(DWordInputBlock, addrOffset, 2, wordOuput);
} else {
res = WriteWordBlock(WordInputBlock, addrOffset, 2, wordOuput);
}
if (res == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")}"));
return res;
}
}
return E1.OK;
}
/// <summary>
/// Write PLC Word Block (Word를 특정 size만큼 쓰기)
/// ** Write 이므로, 기본값은 isOutput = true 임
/// </summary>
/// <param name="addrOffset"></param>
/// <param name="wordSize"></param>
/// <param name="shArray"></param>
/// <param name="isOutput"></param>
/// <returns></returns>
public int WriteWordBlock(int addrOffset, int wordSize, short[] shArray, ePLCDevice device = ePLCDevice.W, bool isOutput = true)
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return E2.NOT_READY;
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.E(WriteLogTag("PLC is not connected"));
return E2.NOT_CONNECTED;
}
int res;
if (isOutput) {
if (device == ePLCDevice.D) {
res = WriteWordBlock(DWordOutputBlock, addrOffset, wordSize, shArray);
} else {
res = WriteWordBlock(WordOutputBlock, addrOffset, wordSize, shArray);
}
if (res == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")} to {wordSize}"));
return res;
}
} else { // Input: 시뮬에서만 write 허용
if (IsSimul != true) { // 에러
Log.E(WriteLogTag("Improper Write WordInput operation in real mode"));
return E2.UNAVAILABLE;
}
if (device == ePLCDevice.D) {
res = WriteWordBlock(DWordInputBlock, addrOffset, wordSize, shArray);
} else {
res = WriteWordBlock(WordInputBlock, addrOffset, wordSize, shArray);
}
if (res == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")} to {wordSize}"));
return res;
}
}
return E1.OK;
}
// For atomic (?) operation
// 근접한, but 양립불가한 Bit 메모리주소를 한번에 Read/Write 하고 싶을 때
// BitBlock API추가 (2가지 버전, bool 배열, ushort 값(비트 단위, 16bit))
// 기본: 이건 bool 배열이므로, size 에 제약 없음
public (int err, bool[] data) ReadBitBlock(int addrOffset, int bitSize, ePLCDevice device = ePLCDevice.B, bool isInput = true)
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return (E2.NOT_READY, null);
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not connected"));
return (E2.NOT_CONNECTED, null);
}
(int err, bool[] data) res2;
if (isInput) {
if (device == ePLCDevice.M) {
res2 = ReadBitBlock(MBitInputBlock, addrOffset, bitSize);
} else {
res2 = ReadBitBlock(BitInputBlock, addrOffset, bitSize);
}
if (res2.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
} else {
if (device == ePLCDevice.M) {
res2 = ReadBitBlock(MBitOutputBlock, addrOffset, bitSize);
} else {
res2 = ReadBitBlock(BitOutputBlock, addrOffset, bitSize);
}
if (res2.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
}
return res2;
}
// 옵션: 비트 데이터이므로, ushort 를 사용함 (bitSize : (0, 16])
public (int err, ushort data) ReadBitBlockVal(int addrOffset, int bitSize, ePLCDevice device = ePLCDevice.B, bool isInput = true)
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return (E2.NOT_READY, 0);
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not connected"));
return (E2.NOT_CONNECTED, 0);
}
(int err, bool[] data) res2Arr;
if (isInput) {
if (device == ePLCDevice.M) {
res2Arr = ReadBitBlock(MBitInputBlock, addrOffset, bitSize);
} else {
res2Arr = ReadBitBlock(BitInputBlock, addrOffset, bitSize);
}
if (res2Arr.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
} else {
if (device == ePLCDevice.M) {
res2Arr = ReadBitBlock(MBitOutputBlock, addrOffset, bitSize);
} else {
res2Arr = ReadBitBlock(BitOutputBlock, addrOffset, bitSize);
}
if (res2Arr.err == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
}
// RW 는 PLC 장치에서 매우 중요한 기능
// RW API 를 잘못 사용할 경우, 장비 세팅과정에서 드러날건데, 문제가 바로 드러나는게 디버깅에 좋을 듯
// 잘못된 주소 범위가 입력되면, data 값이 null 임
// ==> 아래 bool 배열 -> ushort 변환할 필요가 없으므로, 리턴하도록 하자
if (res2Arr.err == E2.INVALID_ARGUMENT) {
return (res2Arr.err, 0);
}
// 예외 체크 : 최대 16 bit 까지만 읽는것을 허용
// 내부에 1 보다 작은지 검사하는 로직이 있음
if (bitSize > 16) { // 에러
Log.E(WriteLogTag($"Invalid size({bitSize}): must be <= 16"));
// 해당 에러코드는 리턴하되, 일단은 16으로 제약을 건다.
res2Arr.err = (int)ePLCErrCode.API_Read_InvalidSize;
bitSize = 16;
}
(int err, ushort data) res2 = (res2Arr.err, 0);
// bool 배열을 비트 순서별로 data 로 변환하기
for (int i = 0; i < bitSize; i++) {
// bool 배열을 자리수에 맞춰서 ushort 로 조합
res2.data += (ushort)((res2Arr.data[i] ? 1 : 0) * (1 << i));
}
return res2;
}
// 기본
public int WriteBitBlock(int addrOffset, int bitSize, bool[] bArray, ePLCDevice device = ePLCDevice.B, bool isOutput = true)
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return E2.NOT_READY;
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.E(WriteLogTag("PLC is not connected"));
return E2.NOT_CONNECTED;
}
int res;
if (isOutput) {
if (device == ePLCDevice.M) {
res = WriteBitBlock(MBitOutputBlock, addrOffset, bitSize, bArray);
} else {
res = WriteBitBlock(BitOutputBlock, addrOffset, bitSize, bArray);
}
if (res == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")} to {bitSize}"));
return res;
}
} else { // Input: 시뮬에서만 write 허용
if (IsSimul != true) { // 에러
Log.E(WriteLogTag("Improper Write BitInput block operation in real mode"));
return E2.UNAVAILABLE;
}
if (device == ePLCDevice.M) {
res = WriteBitBlock(MBitInputBlock, addrOffset, bitSize, bArray);
} else {
res = WriteBitBlock(BitInputBlock, addrOffset, bitSize, bArray);
}
if (res == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")} to {bitSize}"));
return res;
}
}
return E1.OK;
}
// 옵션: 비트 처리는 unsigned 사용
public int WriteBitBlock(int addrOffset, int bitSize, ushort ushData, ePLCDevice device = ePLCDevice.B, bool isOutput = true)
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return E2.NOT_READY;
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.E(WriteLogTag("PLC is not connected"));
return E2.NOT_CONNECTED;
}
int res = E1.OK, resByBitSize = 0;
// 예외 체크 : ushort 를 사용하므로, 16 bit 까지만 허용됨
// 내부에 1 보다 작은지 검사하는 로직이 있음
if (bitSize > 16) { // 에러
Log.E(WriteLogTag($"Invalid size({bitSize}): must be <= 16"));
// 에러코드는 리턴하되, 일단은 16으로 제약을 건다.
resByBitSize = (int)ePLCErrCode.API_Write_InvalidSize;
bitSize = 16;
}
// shData 를 bool 배열로 변환
bool[] bArray = new bool[bitSize];
for (int i = 0; i < bitSize; i++) {
bArray[i] = ((ushData >> i) & 0x1) == 1 ? true : false;
}
if (isOutput) {
if (device == ePLCDevice.M) {
res = WriteBitBlock(MBitOutputBlock, addrOffset, bitSize, bArray);
} else {
res = WriteBitBlock(BitOutputBlock, addrOffset, bitSize, bArray);
}
if (res == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")} to {bitSize}"));
return res;
}
} else { // Input: 시뮬에서만 write 허용
if (IsSimul != true) { // 에러
Log.E(WriteLogTag("Improper Write BitInput block operation in real mode"));
return E2.UNAVAILABLE;
}
if (device == ePLCDevice.M) {
res = WriteBitBlock(MBitInputBlock, addrOffset, bitSize, bArray);
} else {
res = WriteBitBlock(BitInputBlock, addrOffset, bitSize, bArray);
}
if (res == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")} to {bitSize}"));
return res;
}
}
return res + resByBitSize;
}
//===========================================
#endregion
#region 특수 케이스
// B장치 초기화 (모두 0 으로 reset)
public void AllBitClear(ePLCDevice device = ePLCDevice.B, bool isOutput = true)
{
lock (_lockBitInOut) {
if (isOutput) { // 모든 B 출력 초기화
if(device == ePLCDevice.M) {
for (int i = 0; i < MBitOutputBlock.Count; i++) MBitOutputBlock.Data[i] = false;
} else {
for (int i = 0; i < BitOutputBlock.Count; i++) BitOutputBlock.Data[i] = false;
}
} else { // 모든 B 입력 초기화
if(device == ePLCDevice.M) {
for (int i = 0; i < MBitInputBlock.Count; i++) MBitInputBlock.Data[i] = false;
} else {
for (int i = 0; i < BitInputBlock.Count; i++) BitInputBlock.Data[i] = false;
}
}
}
}
// 수동 업데이트 (주의: 업데이트 스레드 시작 전에만 사용. 최초 업데이트 수행)
protected void ManualDataUpdate()
{
DataUpdator();
}
#endregion
#region 주로 모니터링 뷰에서 사용
// 모니터링 뷰에서 필요한 함수
public eSendResult WriteWordBlockForAscii(int addrOffset, int sendWordSize, ref string[] strData, ePLCDevice device = ePLCDevice.W, bool isOutput = true)
{
// 1 워드에 최대 2개의 문자 저장/표현 가능
char[] charTemp = new char[sendWordSize * 2];
short[] sendData = new short[sendWordSize];
try {
// 문자열에서 한 문자씩 char 배열로 분리
for (int i = 0; i < strData[0].Length; i++) {
charTemp[i] = Convert.ToChar(strData[0].Substring(i, 1));
}
// 기본 워드단위로 PLC와 통신함
int j = 0;
for (int i = 0; i < sendWordSize; i++) {
// 2문자 단위로, 1 워드에 저장하자...
j = i * 2;
// 미츠비시 PLC는 lower word -> upper word 순으로 주소값이 순차 증가함
// 2문자 단위로 워드에 저장..
string strTemp = string.Format("{0:X2}", Convert.ToInt16(charTemp[j + 1])) + string.Format("{0:X2}", Convert.ToInt16(charTemp[j]));
sendData[i] = Int16.Parse(strTemp, System.Globalization.NumberStyles.AllowHexSpecifier);
}
int iRet;
if (isOutput) {
if(device == ePLCDevice.D) {
iRet = WriteWordBlock(DWordOutputBlock, addrOffset, sendWordSize, sendData);
} else {
iRet = WriteWordBlock(WordOutputBlock, addrOffset, sendWordSize, sendData);
}
if (iRet == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
} else { // Input: 시뮬에서만 writing 허용
if (IsSimul) {
if(device == ePLCDevice.D) {
iRet = WriteWordBlock(DWordInputBlock, addrOffset, sendWordSize, sendData);
} else {
iRet = WriteWordBlock(WordInputBlock, addrOffset, sendWordSize, sendData);
}
if (iRet == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
} else { // 잘못된 사용...
Log.E(WriteLogTag("Write WordInput in Real mode is wrong"));
return eSendResult.Error_Exception;
}
}
if (iRet != E1.OK) return eSendResult.Error_Communication;
return eSendResult.OK;
} catch (Exception ex) {
Log.E($"Exception: {ex.Message}");
return eSendResult.Error_Exception;
}
}
// 모니터링뷰에서만 사용함...개선 검토: 중복 여부 등
public eSendResult WriteWordBlock(eDataType displayType, int addrOffset, int nCount, in string[] strData, ePLCDevice device = ePLCDevice.W, bool isHex = false, bool isOutput = true)
{
if (!IsActivated) {
if (IsLogModePLCDetail) Log.W(WriteLogTag("PLC is not activated"));
return eSendResult.Error_NotActivated;
}
if (!IsConnected) {
if (IsLogModePLCDetail) Log.E(WriteLogTag("PLC is not connected"));
return eSendResult.Error_NotConnect;
}
int Index = 0;
// 16bit = 0, 32bit = 1, Ascii = 2
// 보낼 개수 * 워드 단위 (16비트 or 32비트)
// 워드 기준 총 사이즈
int iSendLength = nCount * ((int)displayType + 1);
short[] iSendData = new short[iSendLength]; // 메모리 할당
int iRet;
bool bRet;
string strTemp = "";
// try-catch 가 여기서 필요한가?? 통신 API 는 별도의 스레드에서 전담할건데..
try {
// 변환 로직
switch (displayType) {
case eDataType.Type_16Bit: {
// short 기준 보낼 데이터 수
for (int i = 0; i < nCount; ++i) {
// 문자열 숫자를 16비트 정수형으로 변환
if (!isHex) { // 십진수 문자열
// 숫자 문자열인지 검사
if (!IsNumeric(ref strData[i])) return eSendResult.Error_DataIsNumeric;
// 실수이면 에러처리...
if (!Int32.TryParse(strData[i], out int sendData)) {
return eSendResult.Error_DataType;
};
if ((sendData > Int16.MaxValue) || (sendData < Int16.MinValue)) return eSendResult.Error_DataRange;
iSendData[i] = (short)sendData;
} else { // 헥사 문자열
if (strData.Length > 4) return eSendResult.Error_DataLength;
bRet = Int16.TryParse(strData[i], System.Globalization.NumberStyles.AllowHexSpecifier, null, out short sendData);
if (!bRet) return eSendResult.Error_DataType;
iSendData[i] = (short)sendData;
}
}
break;
}
case eDataType.Type_32Bit: {
for (int i = 0; i < nCount; ++i) {
if (!isHex) { // 정수형 문자열
// 숫자인지 검사
if (!IsNumeric(ref strData[i])) return eSendResult.Error_DataIsNumeric;
// 실수이면 에러처리... Int32 범위 초과와 구분하기위해서 Int64로 검사함
if (!Int64.TryParse(strData[i], out var sendData)) {
return eSendResult.Error_DataType;
};
// 32비트 범위 유효 검사
if ((sendData > Int32.MaxValue) || (sendData < Int32.MinValue)) return eSendResult.Error_DataRange;
// 32 비트는 4 바이트 ==> 헥사값으로 8개 (1바이트는 헥사 2개로 표현)
strTemp = string.Format("{0:X8}", int.Parse(strData[i])); // 정수값을 헥사 문자열로 변환 (8개 헥사문자열)
iSendData[Index] = Int16.Parse(StrRight(strTemp, 4), System.Globalization.NumberStyles.AllowHexSpecifier); // lower part
Index++;
iSendData[Index] = Int16.Parse(StrLeft(strTemp, 4), System.Globalization.NumberStyles.AllowHexSpecifier); // upper part
Index++;
} else { // 헥사 문자열
if (strData.Length > 8) return eSendResult.Error_DataLength;
// 실수이면 에러처리... Int32 비트초과와 구분하기위해서 Int64로 검사함
if (!Int64.TryParse(strData[i], System.Globalization.NumberStyles.AllowHexSpecifier, null, out _)) {
return eSendResult.Error_DataType;
};
// 문자열이 작을 시, 0을 패딩하여, 8자리 문자열로 만들기..
strTemp = strData[i].PadLeft(8, '0');
iSendData[Index] = Int16.Parse(StrRight(strTemp, 4), System.Globalization.NumberStyles.AllowHexSpecifier);
Index++;
iSendData[Index] = Int16.Parse(StrLeft(strTemp, 4), System.Globalization.NumberStyles.AllowHexSpecifier);
Index++;
}
}
break;
}
}
if (isOutput) {
if(device == ePLCDevice.D) {
iRet = WriteWordBlock(DWordOutputBlock, addrOffset, (short)iSendLength, iSendData);
} else {
iRet = WriteWordBlock(WordOutputBlock, addrOffset, (short)iSendLength, iSendData);
}
if (iRet == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"Out: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
} else { // Input: 시뮬에서만 write 허용
if (IsSimul) {
if(device == ePLCDevice.D) {
iRet = WriteWordBlock(DWordInputBlock, addrOffset, (short)iSendLength, iSendData);
} else {
iRet = WriteWordBlock(WordInputBlock, addrOffset, (short)iSendLength, iSendData);
}
if (iRet == E2.INVALID_ARGUMENT) {
Log.E(WriteLogTag($"In: Out of Range: 0x{addrOffset.ToString("X4")}"));
}
} else { // 잘못된 사용...
Log.E(WriteLogTag("Write WordInput in Real mode is wrong"));
return eSendResult.Error_Exception;
}
}
if (iRet != E1.OK) return eSendResult.Error_Communication;
return eSendResult.OK;
} catch (Exception ex) {
Log.E($"Exception: {ex.Message}");
return eSendResult.Error_Exception;
}
}
#endregion
#region PLC R/W 동기화용
public ushort LatchForUpdatePointer { get => PLCUpdatorInfo.UpdatePointer; }
public ulong LatchForUpdateCycle { get => PLCUpdatorInfo.NumberOfUpdateCycle; }
public bool WaitWriteBitUpdateUntil(int waitCycle, int timeOutMS, out int elapsedMS)
{
elapsedMS = timeOutMS;
// convert to Max count
int delayMS = (int)(CurConfigUpdateRateMS * 0.1);
if (delayMS < minDelayMS) delayMS = minDelayMS;
Stopwatch _sw = new Stopwatch();
_sw.Start();
if (waitCycle <= 0) { // 예외 처리
waitCycle = 1;
}
// latch
var curUpdateCycle = PLCUpdatorInfo.NumberOfWriteBit + (ulong)waitCycle;
while (_sw.ElapsedMilliseconds < timeOutMS) { // 타임아웃 이내인 경우,
// 업데이트 카운트 체크
if (PLCUpdatorInfo.NumberOfWriteBit > curUpdateCycle) {
elapsedMS = (int)_sw.ElapsedMilliseconds;
// 타이머 종료
_sw.Stop();
return true;
}
// 딜레이..
Thread.Sleep(delayMS);
}
// 타임아웃...
elapsedMS = (int)_sw.ElapsedMilliseconds;
_sw.Stop();
return false;
}
// 비트 출력 이전 단계의 횟수를 저장!!! 즉 워드 출력임
public ulong LatchForWriteBitUpdate { get => PLCUpdatorInfo.NumberOfWriteWord; }
public bool WaitWriteBitUpdate(ulong latchedWriteBitUpdate, out int elapsedMS, int timeOutMS = 1000)
{
elapsedMS = timeOutMS;
// convert to Max count
int delayMS = (int)(CurConfigUpdateRateMS * 0.1);
if (delayMS < minDelayMS) delayMS = minDelayMS;
Stopwatch _sw = new Stopwatch();
_sw.Start();
// latch
// 애매한 상황: WriteWordOutputToPLC 후, WriteBitOutputToPLC 도중에 Write 가 이루어진 경우
// 즉, latchedWriteBitUpdate - numberOfWriteBit > 0 은 현재 WriteBitOutputToPLC 진행 중을 의미함
// => 애매하니(안정적으로) +1 을 추가함
var numberOfWriteBit = PLCUpdatorInfo.NumberOfWriteBit;
var curUpdateCycle = (latchedWriteBitUpdate - numberOfWriteBit) > 0 ? numberOfWriteBit + 1 : numberOfWriteBit;
while (_sw.ElapsedMilliseconds < timeOutMS) { // 타임아웃 이내인 경우,
// 업데이트 카운트 체크
if (PLCUpdatorInfo.NumberOfWriteBit > curUpdateCycle) {
elapsedMS = (int)_sw.ElapsedMilliseconds;
// 타이머 종료
_sw.Stop();
return true;
}
// 딜레이..
Thread.Sleep(delayMS);
}
// 타임아웃...
elapsedMS = (int)_sw.ElapsedMilliseconds;
_sw.Stop();
return false;
}
/*
// 내부 latch 부분 재검토...
public bool WaitWriteBitUpdate(out int elapsedMS, int timeOutMS)
{
elapsedMS = timeOutMS;
// convert to Max count
int delayMS = (int)(curUpdateRateMS * 0.1);
if (delayMS < 10) delayMS = 10;
Stopwatch _sw = new Stopwatch();
_sw.Start();
// latch
// ReadWordInputFromPLC 와 WriteBitOutputToPLC 사이인 경우, 애매하니, +1 을 한다.
var curUpdateCycle = (PLCUpdateTimerInfo.NumberOfReadWord - PLCUpdateTimerInfo.NumberOfWriteBit) > 0 ? PLCUpdateTimerInfo.NumberOfWriteBit + 1 : PLCUpdateTimerInfo.NumberOfWriteBit;
while (_sw.ElapsedMilliseconds < timeOutMS) { // 타임아웃 이내인 경우,
// 업데이트 카운트 체크
if (PLCUpdateTimerInfo.NumberOfWriteBit > curUpdateCycle) {
elapsedMS = (int)_sw.ElapsedMilliseconds;
// 타이머 종료
_sw.Stop();
return true;
}
// 딜레이..
Thread.Sleep(delayMS);
}
// 타임아웃...
elapsedMS = (int)_sw.ElapsedMilliseconds;
_sw.Stop();
return false;
}
*/
// 앞단계 PLC API 업데이트 횟수
public ulong LatchForReadWordUpdate { get => PLCUpdatorInfo.NumberOfWriteBit; }
public bool WaitReadWordUpdate(ulong latchedReadWordUpdate, out int elapsedMS, int timeOutMS = timeOut1000MS)
{
elapsedMS = timeOutMS;
// convert to Max count
int delayMS = (int)(CurConfigUpdateRateMS * 0.1);
if (delayMS < minDelayMS) delayMS = minDelayMS; // MX Component 기준..
Stopwatch _sw = new Stopwatch();
_sw.Start();
// latch
// ReadBitInputFromPLC 와 ReadWordInputFromPLC 사이라면 +1 함
var numberOfReadWord = PLCUpdatorInfo.NumberOfReadWord;
var curUpdateCycle = (latchedReadWordUpdate - numberOfReadWord) > 0 ? numberOfReadWord + 1 : numberOfReadWord;
while (_sw.ElapsedMilliseconds < timeOutMS) { // 타임아웃 이내인 경우,
// 업데이트 카운트 체크
if (PLCUpdatorInfo.NumberOfReadWord > curUpdateCycle) {
elapsedMS = (int)_sw.ElapsedMilliseconds;
// 타이머 종료
_sw.Stop();
return true;
}
// 딜레이..
Thread.Sleep(delayMS);
}
// 타임아웃...
elapsedMS = (int)_sw.ElapsedMilliseconds;
_sw.Stop();
return false;
}
public bool WaitReadWordUpdate(out int elapsedMS, int timeOutMS = timeOut1000MS)
{
elapsedMS = timeOutMS;
// convert to Max count
int delayMS = (int)(CurConfigUpdateRateMS * 0.1);
if (delayMS < minDelayMS) delayMS = minDelayMS;
Stopwatch _sw = new Stopwatch();
_sw.Start();
// latch
// 애매한 상황: 동기화를 하는 순간에 워드 입력이 업데이트 중일때....이런 확률은 거의 없을 것 같은데...
// 이전 단계인 비트 출력 횟수를 래치해야 할까???
// 일단은 이대로 해놓고 모니터링해보자...
var curUpdateCycle = PLCUpdatorInfo.NumberOfReadWord;
while (_sw.ElapsedMilliseconds < timeOutMS) { // 타임아웃 이내인 경우,
// 업데이트 카운트 체크
if (PLCUpdatorInfo.NumberOfReadWord > curUpdateCycle) {
elapsedMS = (int)_sw.ElapsedMilliseconds;
// 타이머 종료
_sw.Stop();
return true;
}
// 딜레이..
Thread.Sleep(delayMS);
}
// 타임아웃...
elapsedMS = (int)_sw.ElapsedMilliseconds;
_sw.Stop();
return false;
}
// 보통 Write Bit 전에 필요한 데이터를 Write Word 로 전달함
// 따라서, WordOutput --> BitOutput 순서를 보장하기 위한 동기화 메서드임
public ulong LatchForWriteWordUpdate { get => PLCUpdatorInfo.NumberOfReadBit; }
public bool WaitWriteWordUpdate(ushort latchedUpdatePointer, ulong latchedUpdateCycle, out int elapsedMS, int timeOutMS = timeOut1000MS)
{
// WO ==> BO 순서를 보장하는게 중요함
// 따라서, Write WO 완료시점을 기준으로 판단
// 노 대기 조건 (PLC 업데이트 스레드 기준)
// 1. WO 전에 완료 (<= 4)
// 2. BO 후에 완료 (>= 8)
// 대기 조건
// 3. WO 도중 (5)
// 4. BO 전 (6)
// 5. BO 도중 (7)
// ==> BO 완료까지 대기
// 노 대기 조건
if (latchedUpdatePointer <= 4 || latchedUpdatePointer == 8) {
elapsedMS = 0;
return true;
}
// 대기 조건 : BO 완료까지 대기 필요
// convert to Max count : Sleep 간격용 (PLC 업데이트 rate 기준, 얼마나 자주 조건 체크를 할건지)
int delayMS = (int)(CurConfigUpdateRateMS * 0.1); // 10배
if (delayMS < minDelayMS) delayMS = minDelayMS; // 하한선
Stopwatch _sw = new Stopwatch();
_sw.Start();
while (_sw.ElapsedMilliseconds < timeOutMS) { // 타임아웃 이내인 경우,
// 업데이트 카운트 체크
if (PLCUpdatorInfo.NumberOfUpdateCycle > latchedUpdateCycle) {
elapsedMS = (int)_sw.ElapsedMilliseconds;
// 타이머 종료
_sw.Stop();
return true;
}
// 딜레이..
Thread.Sleep(delayMS);
}
// 타임아웃...
elapsedMS = (int)_sw.ElapsedMilliseconds;
_sw.Stop();
return false;
}
public bool WaitWriteWordUpdate(out int elapsedMS, int timeOutMS = timeOut1000MS)
{
// 무조건 1번의 WriteWord 를 보장함..
// 주의 영역은 ReadBitInputFromPLC - WriteWordOutputToPLC 구간..
elapsedMS = timeOutMS;
// convert to Max count
int delayMS = (int)(CurConfigUpdateRateMS * 0.1);
if (delayMS < minDelayMS) delayMS = minDelayMS;
Stopwatch _sw = new Stopwatch();
_sw.Start();
// latch
var curUpdateCycle = PLCUpdatorInfo.NumberOfWriteWord;
while (_sw.ElapsedMilliseconds < timeOutMS) { // 타임아웃 이내인 경우,
// 업데이트 카운트 체크
if (PLCUpdatorInfo.NumberOfWriteWord > curUpdateCycle) {
elapsedMS = (int)_sw.ElapsedMilliseconds;
// 타이머 종료
_sw.Stop();
return true;
}
// 딜레이..
Thread.Sleep(delayMS);
}
// 타임아웃...
elapsedMS = (int)_sw.ElapsedMilliseconds;
_sw.Stop();
return false;
}
#endregion
}
}