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 DigitalInputs { get; set; } = new List(); public List DigitalOutputs { get; set; } = new List(); public List DataViewTypeList { get; set; } = new List() { 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> InputMaps = new Dictionary>(); public Dictionary> OutputMaps = new Dictionary>(); 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 input = new List(); List output = new List(); 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 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 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 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 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 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; } } } }