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

743 lines
30 KiB
C#

using Aurora;
using Aurora.Api;
using Aurora.Util;
using Aurora.View;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Input;
using System.Windows.Threading;
using static AuroraPLC.PLCBase;
namespace AuroraPLC.Views
{
public class WordValue : NotifyPropertyChangedBase
{
private string _word;
public string Word {
get { return _word; }
set {
if (_word != value) {
_word = value;
NotifyPropertyChanged("Word");
}
}
}
public string Address { get; set; }
private string _description;
public string Description {
get => _description;
set {
if (_description != value) {
_description = value;
NotifyPropertyChanged("Description");
}
}
}
public int addrOffset { get; set; }
private bool _use = true;
public bool Use {
get { return _use; }
set { SetProperty(ref _use, value); }
} // DataGridRow의 IsEnable과 Binding되는 property
}
public class UCPLCWordViewModel : NotifyPropertyChangedBase
{
Logger Log = new Logger("PLC Mon. Word");
// 32bit 옵션을 지원하기 위해서 (아스키 포함) string 으로 변경...
private string _WORD_DATA;
public string WORD_DATA { get => _WORD_DATA; set => SetProperty(ref _WORD_DATA, value); }
public ICommand CMD_VisibleChanged { get; }
//public ICommand CMD_CalculateFlatness { get; }
public ICommand CMD_ChangeValue { get; }
public List<WordValue> DigitalInputs { get; set; } = new List<WordValue>();
public List<WordValue> DigitalOutputs { get; set; } = new List<WordValue>();
public List<string> DataViewTypeList { get; set; }
= new List<string>() { PLCBase.DisplayFormat.Bit16, PLCBase.DisplayFormat.Bit32Odd, PLCBase.DisplayFormat.Bit32Even, PLCBase.DisplayFormat.ASCII };
public string DataViewTypeInput { get; set; }
public string DataViewTypeOutput { get; set; }
private bool _isCheckedDataViewHexInput = false;
public bool IsCheckedDataViewHexInput { get => _isCheckedDataViewHexInput; set => SetProperty(ref _isCheckedDataViewHexInput, value); }
private bool _isCheckedDataViewHexOutput = false;
public bool IsCheckedDataViewHexOutput { get => _isCheckedDataViewHexOutput; set => SetProperty(ref _isCheckedDataViewHexOutput, value); }
//------------------------
// For Sim Only
public bool IsThisInput { get; private set; }
// Input/Ouput 공용 Item
private WordValue selectedWordItem;
public WordValue SelectedWordItem {
get {
return selectedWordItem;
}
set {
selectedWordItem = value;
}
}
private WordValue selectedInputItem;
public WordValue SelectedInputItem {
get {
return selectedInputItem;
}
set {
// 반대편 datagrid 의 선택을 해제한다.
selectedOutputItem = null;
NotifyPropertyChanged("SelectedOutputItem");
//=====================================
selectedInputItem = value;
IsThisInput = true;
selectedWordItem = value; // For Sim mode : Input bit change
NotifyPropertyChanged("SelectedWordAddress");
}
}
//====================================
private WordValue selectedOutputItem;
public WordValue SelectedOutputItem {
get {
return selectedOutputItem;
}
set {
// 반대편 datagrid 의 선택을 해제한다.
selectedInputItem = null;
NotifyPropertyChanged("SelectedInputItem");
//=====================================
selectedOutputItem = value;
IsThisInput = false;
selectedWordItem = value; // For Sim mode (Input 쪽과 일관성을 위해서)
NotifyPropertyChanged("SelectedWordAddress");
}
}
public string SelectedWordAddress {
get {
return SelectedWordItem?.Address;
}
}
private int selectedInputIndex = 0;
public int SelectedInputIndex {
get => selectedInputIndex;
set {
if (selectedInputIndex != value) {
selectedInputIndex = value;
DigitalInputs = InputMaps[value];
NotifyPropertyChanged("DigitalInputs");
}
}
}
private int selectedOutputIndex = 0;
public int SelectedOutputIndex {
get => selectedOutputIndex;
set {
if (selectedOutputIndex != value) {
selectedOutputIndex = value;
DigitalOutputs = OutputMaps[value];
NotifyPropertyChanged("DigitalOutputs");
}
}
}
private bool isActivated = false;
public bool IsActivated {
get => isActivated;
set {
if (isActivated != value) {
isActivated = value;
NotifyPropertyChanged("IsActivated");
}
}
}
private bool _changeButtonEnable;
public bool ChangeButtonEnable {
get => _changeButtonEnable;
set => SetProperty(ref _changeButtonEnable, value);
}
public Dictionary<int, List<WordValue>> InputMaps = new Dictionary<int, List<WordValue>>();
public Dictionary<int, List<WordValue>> OutputMaps = new Dictionary<int, List<WordValue>>();
private bool isVisible;
private eLangType curLanguage;
private IDeviceEntry DeviceManager;
private readonly PLCBase _plc;
private ePLCDevice _deviceName;
private readonly DispatcherTimer MonitorTimer;
public UCPLCWordViewModel(IAuroraEnvironment aurora, string deviceId, ePLCNetType plcNetType, ePLCDevice device = ePLCDevice.W)
{
DeviceManager = aurora.DeviceManager;
CMD_VisibleChanged = new DelegateCommand(VisibleChanged);
CMD_ChangeValue = new DelegateCommand(ChangeWordValue);
_deviceName = device;
_plc = DeviceManager[deviceId] as PLCBase;
List<WordValue> input = new List<WordValue>();
List<WordValue> output = new List<WordValue>();
InputMaps.Add(0, input);
OutputMaps.Add(0, output);
isVisible = false;
curLanguage = PLCBase.Language;
// Config 로부터 Input/Output 메모리 맵 파싱 및 할당
LoadWordFromConfig();
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).ObserveOnDispatcher()
.Subscribe(x => OnDeviceActiveChanged());
// Log In/Out 시점 파악을 위한 이벤트 관측하기 : For Write 권한 제어
aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_PERMISSION_CHANGED)
.Subscribe(x => UpdateWriteState());
// 언어 변경 이벤트
aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_LANGUAGE_CHANGED)
.Select(msg => msg.Param.ToString().Split('-').First().ToLower())//.Where(lan => lan != _currLanguage)
.ObserveOnDispatcher().Subscribe(lan => UpdateLanguage(lan));
DigitalInputs = InputMaps[0];
DigitalOutputs = OutputMaps[0];
// display 포맷 옵션 초기화
DataViewTypeInput = PLCBase.DisplayFormat.Bit16;
DataViewTypeOutput = PLCBase.DisplayFormat.Bit16;
MonitorTimer = new DispatcherTimer();
MonitorTimer.Tick += OnMonitoring;
MonitorTimer.Interval = TimeSpan.FromMilliseconds(250);
}
private void UpdateLanguage(string lan)
{
// 예외 조건
// 언어가 변경되었는데, 모니터링 뷰 화면이 아닌 경우,
if (isVisible == false) {
} else { // 모니터링 화면인 경우,
// 타이머 중단
MonitorTimer.Stop();
switch (lan) {
case "en": {
UpdateViewLanguage(eLangType.English);
break;
}
case "ko": {
UpdateViewLanguage(eLangType.Korean);
break;
}
}
// 타이머 재시작
MonitorTimer.Start();
}
}
private void UpdateViewLanguage(eLangType langType)
{
int i = 0;
switch (langType) {
case eLangType.English: {
foreach (var ioValue in DigitalInputs) {
if (_deviceName == ePLCDevice.D) {
ioValue.Description = _plc.DWordInputs[i].description.Comment_EN;
} else {
ioValue.Description = _plc.WordInputs[i].description.Comment_EN;
}
++i;
}
i = 0;
foreach (var ioValue in DigitalOutputs) {
if (_deviceName == ePLCDevice.D) {
ioValue.Description = _plc.DWordOutputs[i].description.Comment_EN;
} else {
ioValue.Description = _plc.WordOutputs[i].description.Comment_EN;
}
++i;
}
break;
}
case eLangType.Korean: {
foreach (var ioValue in DigitalInputs) {
if (_deviceName == ePLCDevice.D) {
ioValue.Description = _plc.DWordInputs[i].description.Comment_KR;
} else {
ioValue.Description = _plc.WordInputs[i].description.Comment_KR;
}
++i;
}
i = 0;
foreach (var ioValue in DigitalOutputs) {
if (_deviceName == ePLCDevice.D) {
ioValue.Description = _plc.DWordOutputs[i].description.Comment_KR;
} else {
ioValue.Description = _plc.WordOutputs[i].description.Comment_KR;
}
++i;
}
break;
}
}
// 뷰모델 및 PLC 언어도 업데이트
PLCBase.Language = langType;
curLanguage = langType;
}
private void UpdateWriteState() // 화면 내에서 사용자 권한이 변경되었을 경우
{
/*
var curPermissionLevel = ApplicationExt.PermissionLevel;
if (curPermissionLevel > (ushort)Aurora.Api.PermissionLevel.LEVEL3 || IsActivated == false) { // 권한 부족
ChangeButtonEnable = false;
} else {
if (PLC.IsViewEnabled) {
ChangeButtonEnable = true;
} else {
ChangeButtonEnable = false;
}
}
*/
// Auto 모드인경우, Write 불가
if (PLCStatic.IsViewEnabled) {
ChangeButtonEnable = true;
} else {
ChangeButtonEnable = false;
}
}
private void VisibleChanged(object obj) // 화면 전환 시 동작하는 곳
{
UpdateWriteState();
isVisible = (bool)obj;
if (isVisible) {
// 뷰모델이 가지고 있는 언어와 현재 Aurora 언어가 다르면, 설명 언어를 변경함
if (curLanguage != _plc.GetLangTypeFrom(ApplicationExt.CurLang)) {
UpdateViewLanguage(PLCBase.Language);
}
MonitorTimer.Start();
} else {
MonitorTimer.Stop();
}
}
private void OnDeviceActiveChanged()
{
IsActivated = _plc.IsActivated;
if (IsActivated == false) {
ChangeButtonEnable = false;
}
}
private void LoadWordFromConfig()
{
if (_deviceName == ePLCDevice.D) {
// Word Input (D)
List<WordValue> InPutlist = InputMaps[0];
for (int i = 0; i < _plc.DWordInputs.Count; i++) {
var inputW = _plc.DWordInputs[i];
var address = inputW.Item1;
string desc = null;
switch (curLanguage) {
case eLangType.English:
desc = inputW.Item2.Comment_EN;
break;
case eLangType.Korean:
desc = inputW.Item2.Comment_KR;
break;
}
desc?.Trim();
string strAddress = "D" + address.ToString();
var ioValue = new WordValue() {
Address = strAddress,
Description = desc,
//hexAddress = strAddress.Substring(1, strAddress.Length - 1),
//"주소 - 시작주소"가 아니라, 인덱스를 사용
addrOffset = i,
Use = true
};
InPutlist.Add(ioValue);
}
// Word Output
List<WordValue> OutPutlist = OutputMaps[0];
for (int i = 0; i < _plc.DWordOutputs.Count; i++) {
var outputW = _plc.DWordOutputs[i];
var address = outputW.Item1;
string desc = null;
switch (curLanguage) {
case eLangType.English:
desc = outputW.Item2.Comment_EN;
break;
case eLangType.Korean:
desc = outputW.Item2.Comment_KR;
break;
}
desc?.Trim();
string strAddress = "D" + address.ToString();
var ioValue = new WordValue() {
Address = strAddress,
Description = desc,
//"주소 - 시작주소"가 아니라, 인덱스를 사용
addrOffset = i,
Use = true
};
OutPutlist.Add(ioValue);
}
} else {
// Word Input
var startAddress = _plc.WordInputs.Count != 0 ? _plc.WordInputs.OrderBy(x => x.address).First().address : 0;
foreach (var inputW in _plc.WordInputs) {
// 주소와 설명문으로 파싱
//var (address, desc) = inputW;
var address = inputW.Item1;
string desc = null;
switch (curLanguage) {
case eLangType.English: {
desc = inputW.Item2.Comment_EN;
break;
}
case eLangType.Korean: {
desc = inputW.Item2.Comment_KR;
break;
}
}
//desc.Trim();
desc?.Trim();
List<WordValue> InPutlist = InputMaps[0];
string strAddress = "W" + address.ToString("X4"); // convert to Hex string (4 자리)
var ioValue = new WordValue() {
Address = strAddress,
Description = desc,
//hexAddress = strAddress.Substring(1, strAddress.Length - 1),
addrOffset = address - startAddress,
Use = true // 특정 Value를 사용하지 않을 경우 false로 설정 (DataGridRow의 IsEnabled와 연동)
};
// 저장
//InPutlist.AddRange(TempInPutlist);
InPutlist.Add(ioValue);
}
// Word Output
startAddress = _plc.WordOutputs.Count != 0 ? _plc.WordOutputs.OrderBy(x => x.address).First().address : 0;
foreach (var outputW in _plc.WordOutputs) {
// 주소와 설명문으로 파싱
//var (address, desc) = outputW;
var address = outputW.Item1;
string desc = null;
switch (curLanguage) {
case eLangType.English: {
desc = outputW.Item2.Comment_EN;
break;
}
case eLangType.Korean: {
desc = outputW.Item2.Comment_KR;
break;
}
}
desc?.Trim();
List<WordValue> OutPutlist = OutputMaps[0];
string strAddress = "W" + address.ToString("X4"); // convert to Hex string (4자리)
var ioValue = new WordValue() {
Address = strAddress,
Description = desc,
//hexAddress = strAddress.Substring(1, strAddress.Length - 1),
addrOffset = address - startAddress,
Use = true // 특정 Value를 사용하지 않을 경우 false로 설정 (DataGridRow의 IsEnabled와 연동)
};
// 저장
//InPutlist.AddRange(TempInPutlist);
OutPutlist.Add(ioValue);
}
}
}
private void OnMonitoring(object sender, EventArgs e)
{
if (!_plc.IsActivated) {
return;
}
// 뷰 상태 모니터링 : write 버튼 비활성화
UpdateWriteState();
DataViewTypePLCMonitoring(DigitalInputs, true);
DataViewTypePLCMonitoring(DigitalOutputs, false);
}
private void DataViewTypePLCMonitoring(List<WordValue> IOList, bool IsInput)
{
int index = 0;
if (IOList.Count <= 0) {
return;
}
// 기존: memAddress + Count 로 한 번에 블럭 read
// var memAddress = IOList[0].addrOffset;
// (int err, short[] data) res;
// if (_deviceName == ePLCDevice.D) { ... ReadWordBlock ... }
// 변경: 내부 버퍼에서 주소별로 한 칸씩 읽기
var data = new short[IOList.Count];
foreach (var io in IOList) {
(int err, short data) resOne;
if (_deviceName == ePLCDevice.D) {
resOne = _plc.ReadWord(io.addrOffset, ePLCDevice.D, isInput: IsInput);
} else {
resOne = _plc.ReadWord(io.addrOffset, isInput: IsInput);
}
data[index++] = resOne.data;
}
index = 0;
string dataViewType = IsInput ? DataViewTypeInput : DataViewTypeOutput;
bool hexType = IsInput ? IsCheckedDataViewHexInput : IsCheckedDataViewHexOutput;
string tempStrComp = "";
try {
foreach (var ioValue in IOList) {
switch (dataViewType) {
case PLCBase.DisplayFormat.Bit32Odd:
if (index % 2 == 1) {
ioValue.Use = true;
// 메모리 주소가 홀수 - 짝수로 쌍을 이룸
// 마지막 주소가 버퍼 범위를 벗어나는지 검사 필요..
// 미츠비시 PLC 는 big endigan 임. lower word -> upper word 순임
if (index + 1 < data.Length) {
tempStrComp = _plc.Conv_ReadData(PLCBase.eDataType.Type_32Bit, data[index], data[index + 1], hexType);
} else {
tempStrComp = _plc.Conv_ReadData(PLCBase.eDataType.Type_32Bit, data[index], 0, hexType);
}
if (tempStrComp != ioValue.Word) {
ioValue.Word = tempStrComp;
}
} else {
ioValue.Word = "";
ioValue.Use = false;
}
break;
case PLCBase.DisplayFormat.Bit32Even:
if (index % 2 == 0) {
ioValue.Use = true;
if (index + 1 < data.Length) {
tempStrComp = _plc.Conv_ReadData(PLCBase.eDataType.Type_32Bit, data[index], data[index + 1], hexType);
} else {
tempStrComp = _plc.Conv_ReadData(PLCBase.eDataType.Type_32Bit, data[index], 0, hexType);
}
if (tempStrComp != ioValue.Word) {
ioValue.Word = tempStrComp;
}
} else {
ioValue.Use = false;
ioValue.Word = "";
}
break;
case PLCBase.DisplayFormat.ASCII:
ioValue.Use = true;
tempStrComp = _plc.Conv_ReadData(PLCBase.eDataType.Type_Ascii, data[index], 0, false,
true);
if (tempStrComp != ioValue.Word) {
ioValue.Word = tempStrComp;
}
break;
default: // 16 bit
ioValue.Use = true;
ioValue.Word = hexType ? data[index].ToString("X4") : data[index].ToString();
break;
}
index++;
}
} catch (Exception ex) {
Log.E($"Exception: {ex.Message}");
}
}
private void ChangeWordValue(object obj)
{
if (!_plc.IsActivated) {
return;
}
/*
// 사용자 권한에 따라서, 아예 해당 버튼을 disable 하는 방식으로 권한 검사를 처리함
// 방어용으로 이중 처리 (삭제 가능함)
var curPermissionLevel = ApplicationExt.PermissionLevel;
if (curPermissionLevel > (ushort)Aurora.Api.PermissionLevel.LEVEL3) { // 권한 부족
return;
}
// PLC 가 Auto 모드이면 Write 불가
if (PLC.CheckProcessAutoMode()) {
return;
}
*/
// 비고. 리얼 모드에서는 입력의 수정 불가: 리얼모드 & Input 은 무시한다...
if (_plc.IsSimul == false && IsThisInput) {
return;
}
// 빈문자열도 체크해야 할까??
if (SelectedWordItem == null || WORD_DATA == null) {
return;
}
PLCBase.eSendResult eRet = PLCBase.eSendResult.OK;
int iSendSize = 1;
//string strSendData = WORD_DATA;
string[] strSendData = { WORD_DATA };
// display 옵션에 따른 구현
// 32bit odd, even 은 무조건 현재 선택된 주소가 low word 이고, 다음 주소가 high word 임.
string dataViewType = IsThisInput ? DataViewTypeInput : DataViewTypeOutput;
bool hexType = IsThisInput ? IsCheckedDataViewHexInput : IsCheckedDataViewHexOutput;
switch (dataViewType) {
case PLCBase.DisplayFormat.Bit16: {
if (_plc.IsSimul && IsThisInput) { // 시뮬 & 입력부분
eRet = _plc.WriteWordBlock(PLCBase.eDataType.Type_16Bit, SelectedWordItem.addrOffset, iSendSize, in strSendData, _deviceName, hexType, isOutput: false);
} else {
eRet = _plc.WriteWordBlock(PLCBase.eDataType.Type_16Bit, SelectedWordItem.addrOffset, iSendSize, in strSendData, _deviceName, hexType);
}
break;
}
case PLCBase.DisplayFormat.ASCII: {
// LUKAS: ABCDEFGH 를 입력할 경우, AB 가 1워드에 들어감, A 가 lower word임
// 예외1. 빈 문자열이면, 어떻게 하지..?? 일단은 선택된 메모리 주소만 널문자 주도록 하자 !!!
if (strSendData[0].Length == 0) { // 빈 문자열이라면,
strSendData[0] = "\0\0";
}
// write 할 워드 수는 문자열 길이의 절반... 1워드에 2개의 문자가 가능함
// 1 문자 --> 1 워드
// 2 문자 --> 1 워드
// 3 문자 --> 2 워드
// 4 문자 --> 2 워드
// ==> 2로 나눈 후, 올림처리
//iSendSize = (int)Math.Round(strSendData.Length / 2.0 + 0.5); // 오사오입 방식 반올림: 0.5 단위일 경우, 앞자리를 짝수로 만든다: 0.5 => 0, 1.5 = 2
iSendSize = (int)Math.Ceiling(strSendData[0].Length / 2.0);
// 예외 2. 보낼 워드 사이즈가 정의된 메모리 범위를 벗어날 경우, write 불가하고, 에러 로그를 남긴다.
if (_plc.IsSimul && IsThisInput) { // 시뮬 & 입력부분
if (DigitalInputs.Last().addrOffset < SelectedWordItem.addrOffset + iSendSize - 1) {
Log.E("ASCII write is impossible: Out of Word Input address range");
break;
}
eRet = _plc.WriteWordBlockForAscii(SelectedWordItem.addrOffset, iSendSize, ref strSendData, _deviceName, isOutput: false);
} else {
if (DigitalOutputs.Last().addrOffset < SelectedWordItem.addrOffset + iSendSize - 1) {
Log.E("ASCII write is impossible: Out of Word Output address range");
break;
}
eRet = _plc.WriteWordBlockForAscii(SelectedWordItem.addrOffset, iSendSize, ref strSendData, _deviceName);
}
break;
}
default: // 32 bit = 2 워드
// 검토사항: Odd/Even 과 총 메모리 주소의 홀짝 여부에 따라서, 마지막 주소가 선택될 수 있는데, 이 경우, 메모리 주소를 벗어날 수 있다.
// 선택된 주소가 메모리의 마지막 주소인 경우는 write 불가 (정의된 메모리 범위를 벗어나서 write 를 하는 행위이므로..)
if (_plc.IsSimul && IsThisInput) { // 시뮬 & 입력부분
if (DigitalInputs.Last().addrOffset == SelectedWordItem.addrOffset) {
Log.E("32bit write is impossible: upper word is out of address range");
break;
}
eRet = _plc.WriteWordBlock(PLCBase.eDataType.Type_32Bit, SelectedWordItem.addrOffset, iSendSize, in strSendData, _deviceName, hexType, isOutput: false);
} else { // 리얼모드
// 만약 선택된 주소가 마지막 주소라면, write 불가
if (DigitalOutputs.Last().addrOffset == SelectedWordItem.addrOffset) {
Log.E("32bit write is impossible: upper word is out of address range");
break;
}
eRet = _plc.WriteWordBlock(PLCBase.eDataType.Type_32Bit, SelectedWordItem.addrOffset, iSendSize, in strSendData, _deviceName, hexType);
}
break;
}
switch (eRet) {
case PLCBase.eSendResult.Error_NotActivated:
Log.E("PLC is not activated");
break;
case PLCBase.eSendResult.Error_NotConnect:
Log.E("PLC is not connected");
break;
case PLCBase.eSendResult.Error_Communication:
Log.E("PLC Comm Error");
break;
case PLCBase.eSendResult.Error_DataIsNumeric:
Log.E("Only numeric input is allowed");
break;
case PLCBase.eSendResult.Error_DataRange:
Log.E("Out of input range");
break;
case PLCBase.eSendResult.Error_DataLength:
Log.E("Out of input max. length");
break;
case PLCBase.eSendResult.Error_DataType:
Log.E("Input data type is invalid");
break;
case PLCBase.eSendResult.Error_Exception:
Log.E("Exception is occured");
break;
}
}
}
}