//****************************************************************************************************** // MainForm.cs - Gbtc // // Copyright © 2020, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this // file except in compliance with the License. You may obtain a copy of the License at: // // http://opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 11/06/2020 - J. Ritchie Carroll // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; using System.Linq; using System.Windows.Forms; using AdapterExplorer.Model; using GSF; using GSF.Communication; using GSF.ComponentModel; using GSF.Data; using GSF.Data.Model; using GSF.Diagnostics; using GSF.ServiceProcess; using GSF.Threading; using GSF.TimeSeries; using GSF.TimeSeries.Adapters; using GSF.TimeSeries.Transport; using GSF.TimeSeries.UI; using GSF.Windows.Forms; using Measurement = AdapterExplorer.Model.Measurement; namespace AdapterExplorer { public partial class MainForm : SecureForm { private DataSet m_dataSource; private readonly LogPublisher m_log; private AdoDataConnection m_database; private WindowsServiceClient m_windowsServiceClient; private Settings m_settings; private readonly ConcurrentQueue m_subscriberOperationQueue; private readonly ShortSynchronizedOperation m_subscriberOperations; private DataSubscriber m_subscriber; private UnsynchronizedSubscriptionInfo m_throttledSubscription; private Guid[] m_inputSignalIDs; private Guid[] m_outputSignalIDs; private long m_lastRefresh; private bool m_formLoaded; private volatile bool m_formClosing; public MainForm() { InitializeComponent(); m_log = Program.Log; m_subscriberOperationQueue = new ConcurrentQueue(); m_subscriberOperations = new ShortSynchronizedOperation(() => { while (!m_formClosing && m_subscriberOperationQueue.TryDequeue(out Action operation)) { if (m_subscriber.IsConnected) operation(); } }, ex => ShowUpdateMessage($"ERROR: Operations queue exception: {ex.Message}")); groupBoxInputMeasurements.Tag = groupBoxInputMeasurements.Text; groupBoxOutputMeasurements.Tag = groupBoxOutputMeasurements.Text; } private void MainForm_Load(object sender, EventArgs e) { try { // Set current principal and attempt host service connection CommonFunctions.CurrentPrincipal = SecurityPrincipal; CommonFunctions.ServiceConnectionRefreshed += CommonFunctions_ServiceConnectionRefreshed; Program.HostNodeID.SetAsCurrentNodeID(); } catch (Exception ex) { ShowUpdateMessage($"ERROR: Failed to initialize service connection: {ex.Message}"); m_log.Publish(MessageLevel.Error, "Initialize Service Connection", exception: ex); } try { Text += " - " + SecurityPrincipal.Identity.Provider.UserData.LoginID; m_database = Program.GetDatabaseConnection(); if (m_database is null) throw new InvalidOperationException("Failed to connect to database."); MeasurementKey.EstablishDefaultCache(m_database.Connection, m_database.AdapterType); // Load current settings registering a symbolic reference to this form instance for use by default value expressions m_settings = new Settings(new Dictionary { { "Form", this } }.RegisterSymbols()); // Restore last window size/location this.RestoreLayout(); LoadDataSource(); InitializeDataGrid(dataGridViewInputMeasurements); InitializeDataGrid(dataGridViewOutputMeasurements); InitializeDataSubscriber(); LoadAdapters(false); SetBottomPanelWidths(); m_formLoaded = true; } catch (Exception ex) { ShowUpdateMessage($"ERROR: Failed during load: {ex.Message}"); m_log.Publish(MessageLevel.Error, "FormLoad", exception: ex); #if DEBUG throw; #endif } } private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { try { m_formClosing = true; // Save current window size/location this.SaveLayout(); // Save any updates to current screen values m_settings.Save(); } catch (Exception ex) { m_log.Publish(MessageLevel.Error, "FormClosing", "Failed while saving settings", exception: ex); #if DEBUG throw; #endif } } private void MainForm_FormClosed(object sender, FormClosedEventArgs e) { CommonFunctions.DisconnectWindowsServiceClient(); if (!(m_subscriber is null)) { m_subscriber.Stop(); m_subscriber.StatusMessage -= Subscriber_StatusMessage; m_subscriber.ProcessException -= Subscriber_ProcessException; m_subscriber.ConnectionTerminated -= Subscriber_ConnectionTerminated; m_subscriber.NewMeasurements -= Subscriber_NewMeasurements; m_subscriber.ServerConfigurationChanged -= Subscriber_ServerConfigurationChanged; m_subscriber.Dispose(); } m_settings?.Dispose(); } private void FormElementChanged(object sender, EventArgs e) { if (InvokeRequired) { BeginInvoke(new Action(FormElementChanged), sender, e); } else { if (Visible && m_formLoaded) m_settings?.UpdateProperties(); } } private void checkBoxAdapters_CheckedChanged(object sender, EventArgs e) { if (!Visible || !m_formLoaded) return; LoadAdapters(false); FormElementChanged(sender, e); } private bool IsConnected { get; set; } private void QueueSubscriberOperation(Action operation) { m_subscriberOperationQueue.Enqueue(operation); m_subscriberOperations.RunOnceAsync(); } private void CommonFunctions_ServiceConnectionRefreshed(object sender, EventArgs e) { ConnectToService(); } private void ConnectToService() { ClientHelper helper = m_windowsServiceClient?.Helper; ClientBase remotingClient = m_windowsServiceClient?.Helper?.RemotingClient; if (!(helper is null)) helper.ReceivedServiceResponse -= Helper_ReceivedServiceResponse; if (!(remotingClient is null)) { remotingClient.ConnectionTerminated -= RemotingClient_ConnectionTerminated; remotingClient.ConnectionEstablished -= RemotingClient_ConnectionEstablished; } m_windowsServiceClient = CommonFunctions.GetWindowsServiceClient(); helper = m_windowsServiceClient?.Helper; remotingClient = m_windowsServiceClient?.Helper?.RemotingClient; if (m_windowsServiceClient is null || helper is null || remotingClient is null) return; helper.ReceivedServiceResponse += Helper_ReceivedServiceResponse; remotingClient.ConnectionEstablished += RemotingClient_ConnectionEstablished; remotingClient.ConnectionTerminated += RemotingClient_ConnectionTerminated; SetConnectionStatus(remotingClient.CurrentState == ClientState.Connected ? Status.Connected : Status.Disconnected); } private void Helper_ReceivedServiceResponse(object sender, EventArgs e) { if (!ClientHelper.TryParseActionableResponse(e.Argument, out string sourceCommand, out _)) return; Guid[] parseSignalIDs() { try { List attachments = e.Argument.Attachments; if (attachments is null || attachments.Count < 2 || !(attachments[0] is byte[][] signalIDs)) return Array.Empty(); return signalIDs.Select(bytes => new Guid(bytes)).ToArray(); } catch (Exception ex) { m_log.Publish(MessageLevel.Info, "Parse actionable service response with Guid buffers", exception: ex); return Array.Empty(); } } string command = sourceCommand.ToLower().Trim(); if (command.Equals("getinputmeasurements")) { m_inputSignalIDs = parseSignalIDs(); LoadMeasurements(m_inputSignalIDs, dataGridViewInputMeasurements, groupBoxInputMeasurements); } else if (command.Equals("getoutputmeasurements")) { m_outputSignalIDs = parseSignalIDs(); LoadMeasurements(m_outputSignalIDs, dataGridViewOutputMeasurements, groupBoxOutputMeasurements); InitiateSubscribe(); } } private void RemotingClient_ConnectionTerminated(object sender, EventArgs e) { SetConnectionStatus(Status.Disconnected); } private void RemotingClient_ConnectionEstablished(object sender, EventArgs e) { SetConnectionStatus(Status.Connected); } private void SetConnectionStatus(Status status) { if (m_formClosing) return; if (InvokeRequired) { BeginInvoke(new Action(SetConnectionStatus), status); } else { IsConnected = status == Status.Connected; pictureBoxStatus.Image = imageListStatus.Images[(int)status]; toolTip.SetToolTip(pictureBoxStatus, status.ToString()); } } private void LoadDataSource() { m_dataSource = m_database.RetrieveDataSet("SELECT * FROM ActiveMeasurement"); m_dataSource.Tables[0].TableName = "ActiveMeasurements"; } private void InitializeDataSubscriber() { m_throttledSubscription = new UnsynchronizedSubscriptionInfo(true) { LagTime = 10.0D, LeadTime = 10.0D, PublishInterval = 0.5D }; m_subscriber = new DataSubscriber { ConnectionString = $"server=localhost:{Program.GetGEPPort()}", CompressionModes = CompressionModes.TSSC | CompressionModes.GZip, OperationalModes = OperationalModes.UseCommonSerializationFormat | OperationalModes.CompressMetadata | OperationalModes.CompressSignalIndexCache | OperationalModes.CompressPayloadData }; m_subscriber.StatusMessage += Subscriber_StatusMessage; m_subscriber.ProcessException += Subscriber_ProcessException; m_subscriber.ConnectionEstablished += Subscriber_ConnectionEstablished; m_subscriber.ConnectionTerminated += Subscriber_ConnectionTerminated; m_subscriber.NewMeasurements += Subscriber_NewMeasurements; m_subscriber.ServerConfigurationChanged += Subscriber_ServerConfigurationChanged; m_subscriber.Initialize(); m_subscriber.Start(); } private void Subscriber_StatusMessage(object sender, EventArgs e) { ShowUpdateMessage(e.Argument); } private void Subscriber_ProcessException(object sender, EventArgs e) { ShowUpdateMessage($"ERROR: {e.Argument}", false); m_log.Publish(MessageLevel.Info, "GEP Process Exception", exception: e.Argument); } private void Subscriber_ConnectionEstablished(object sender, EventArgs e) { if (m_formClosing) return; InitiateSubscribe(); } private void Subscriber_ConnectionTerminated(object sender, EventArgs e) { if (m_formClosing) return; ShowUpdateMessage("Connection to publisher was terminated, restarting connection cycle..."); QueueSubscriberOperation(() => m_subscriber.Start()); } private void Subscriber_NewMeasurements(object sender, EventArgs> e) { if (m_formClosing) return; Dictionary receivedMeasurements = new Dictionary(); foreach (IMeasurement measurement in e.Argument) receivedMeasurements[measurement.ID] = measurement; AssignMeasurements(receivedMeasurements, dataGridViewInputMeasurements); AssignMeasurements(receivedMeasurements, dataGridViewOutputMeasurements); if (new TimeSpan(DateTime.UtcNow.Ticks - m_lastRefresh).TotalSeconds < 1.0D) return; BeginInvoke(new Action(dataGridViewInputMeasurements.Refresh)); BeginInvoke(new Action(dataGridViewOutputMeasurements.Refresh)); m_lastRefresh = DateTime.UtcNow.Ticks; } private void Subscriber_ServerConfigurationChanged(object sender, EventArgs e) { LoadAdapters(true); } private void InitiateSubscribe() { bool hasInputIDs = m_inputSignalIDs?.Length > 0; bool hasOutputIDs = m_outputSignalIDs?.Length > 0; if (!hasInputIDs && !hasOutputIDs) return; HashSet signalIDs = new HashSet(); if (hasInputIDs) signalIDs.UnionWith(m_inputSignalIDs); if (hasOutputIDs) signalIDs.UnionWith(m_outputSignalIDs); UnsynchronizedSubscriptionInfo subscription = (UnsynchronizedSubscriptionInfo)m_throttledSubscription.Copy(); subscription.FilterExpression = string.Join(";", signalIDs); QueueSubscriberOperation(() => { m_lastRefresh = 0L; m_subscriber.UnsynchronizedSubscribe(subscription); }); } private void AssignMeasurements(Dictionary receivedMeasurements, DataGridView dataGridView) { if (!(dataGridView.DataSource is IList source)) return; foreach (Measurement measurement in source) { if (!receivedMeasurements.TryGetValue(measurement.SignalID, out IMeasurement receivedMeasurement)) continue; measurement.Value = receivedMeasurement.AdjustedValue; measurement.Timestamp = receivedMeasurement.Timestamp; } } private void LoadAdapters(bool restoreSelectedIndex) { if (m_formClosing) return; if (InvokeRequired) { BeginInvoke(new Action(LoadAdapters), restoreSelectedIndex); } else { int selectedIndex = comboBoxAdapters.SelectedIndex; comboBoxAdapters.Items.Clear(); comboBoxAdapters.Text = ""; void loadAdapters(ITableOperations table) { foreach (IIaonAdapter iaonAdapter in table.QueryRecords("AdapterName")) { if (iaonAdapter is null) continue; comboBoxAdapters.Items.Add(iaonAdapter); } } if (checkBoxActionAdapters.Checked) loadAdapters(new TableOperations(m_database)); if (checkBoxInputAdapters.Checked) loadAdapters(new TableOperations(m_database)); if (checkBoxOutputAdapters.Checked) loadAdapters(new TableOperations(m_database)); int itemCount = comboBoxAdapters.Items.Count; if (itemCount == 0) return; if (restoreSelectedIndex && selectedIndex > -1 && selectedIndex < itemCount) comboBoxAdapters.SelectedIndex = selectedIndex; else comboBoxAdapters.SelectedIndex = 0; } } private void AssignInputMeasurements(Dictionary settings) { if (!settings.ContainsKey("inputMeasurementKeys") && settings.TryGetValue("variableList", out string variableList)) { Dictionary variables = variableList.ParseKeyValuePairs(); if (variables.Count > 0) settings["inputMeasurementKeys"] = string.Join(";", variables.Values); } MeasurementKey[] inputMeasurementKeys = settings.TryGetValue("inputMeasurementKeys", out string setting) ? AdapterBase.ParseInputMeasurementKeys(m_dataSource, false, setting) : Array.Empty(); m_inputSignalIDs = inputMeasurementKeys.Select(k => k.SignalID).ToArray(); LoadMeasurements(m_inputSignalIDs, dataGridViewInputMeasurements, groupBoxInputMeasurements); } private void AssignOutputMeasurements(Dictionary settings) { MeasurementKey[] outputMeasurementKeys = settings.TryGetValue("outputMeasurements", out string setting) ? AdapterBase.ParseOutputMeasurements(m_dataSource, false, setting).MeasurementKeys() : Array.Empty(); m_outputSignalIDs = outputMeasurementKeys.Select(k => k.SignalID).ToArray(); LoadMeasurements(m_outputSignalIDs, dataGridViewOutputMeasurements, groupBoxOutputMeasurements); } private void LoadMeasurements(Guid[] signalIDs, DataGridView dataGridView, GroupBox groupBox) { if (m_formClosing) return; if (InvokeRequired) { BeginInvoke(new Action(LoadMeasurements), signalIDs, dataGridView, groupBox); } else { TableOperations measurementTable = new TableOperations(m_database); string signalIDQuery = signalIDs.Length == 0 ? null : $"SignalID IN ({string.Join(",", signalIDs.Select(id => $"'{id}'"))})"; Measurement[] measurements = signalIDQuery is null ? Array.Empty() : measurementTable.QueryRecords("PointTag", new RecordRestriction(signalIDQuery)).ToArray(); dataGridView.DataSource = null; dataGridView.Rows.Clear(); dataGridView.Refresh(); if (measurements.Length > 0) { BindingSource source = new BindingSource(); foreach (Measurement measurement in measurements) source.Add(measurement); dataGridView.DataSource = source; } groupBox.Text = $"{groupBox.Tag} ({measurements.Length:N0}):"; } } private void comboBoxAdapters_SelectedIndexChanged(object sender, EventArgs e) { if (m_formClosing) return; if (comboBoxAdapters.SelectedIndex < 0 || !(comboBoxAdapters.SelectedItem is IIaonAdapter iaonAdapter)) return; Dictionary settings = iaonAdapter.ConnectionString?.ParseKeyValuePairs(); QueueSubscriberOperation(() => m_subscriber.Unsubscribe()); ClearUpdateMessages(); if (IsConnected) { try { // When connected to service, request dynamically assigned runtime measurements directly from adapter CommonFunctions.SendCommandToService($"GetInputMeasurements {iaonAdapter.AdapterName} -actionable"); CommonFunctions.SendCommandToService($"GetOutputMeasurements {iaonAdapter.AdapterName} -actionable"); } catch (Exception ex) { ShowUpdateMessage($"ERROR: Failed to send command to service: {ex.Message}"); } } else { if (!(settings is null)) { // If service is not running, load configured measurements from database - this will work when service is not connected, but is not exhaustive AssignInputMeasurements(settings); AssignOutputMeasurements(settings); InitiateSubscribe(); } } textBoxAdapterInfo.Text = $"Adapter Info: {iaonAdapter.TypeName} [{iaonAdapter.AssemblyName}]{Environment.NewLine}{Environment.NewLine}Connection String:{Environment.NewLine}{Environment.NewLine}{iaonAdapter.ConnectionString}"; } private void MainForm_Resize(object sender, EventArgs e) { splitContainerMeasurements.SplitterDistance = (int)(splitContainerMeasurements.ClientSize.Width * 0.5); SetBottomPanelWidths(); } private void splitContainerMeasurements_SplitterMoved(object sender, SplitterEventArgs e) { SetBottomPanelWidths(); } private void SetBottomPanelWidths() { // ReSharper disable once InconsistentlySynchronizedField textBoxMessageOutput.Width = groupBoxOutputMeasurements.Width; textBoxAdapterInfo.Width = groupBoxInputMeasurements.Width; } private void buttonClear_Click(object sender, EventArgs e) { ClearUpdateMessages(); } private void InitializeDataGrid(DataGridView dataGridView) { DataGridViewCellStyle cellStyle = new DataGridViewCellStyle(); dataGridView.Columns.Clear(); dataGridView.SelectionMode = DataGridViewSelectionMode.CellSelect; dataGridView.AutoGenerateColumns = false; dataGridView.ColumnHeadersVisible = true; dataGridView.ColumnHeadersDefaultCellStyle = cellStyle; dataGridView.RowsDefaultCellStyle = cellStyle; dataGridView.AlternatingRowsDefaultCellStyle = cellStyle; dataGridView.DataBindings.DefaultDataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged; dataGridView.Columns.Add(new DataGridViewColumn { Name = "PointTag", DataPropertyName = "PointTag", HeaderText = "Point Tag", DefaultCellStyle = cellStyle, ReadOnly = true, Resizable = DataGridViewTriState.True, AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, FillWeight = 50, CellTemplate = new DataGridViewTextBoxCell() }); dataGridView.Columns.Add(new DataGridViewColumn { Name = "Value", DataPropertyName = "Value", DefaultCellStyle = new DataGridViewCellStyle(cellStyle) { Format = "N3" }, ReadOnly = true, Resizable = DataGridViewTriState.True, AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, FillWeight = 20, CellTemplate = new DataGridViewTextBoxCell(), }); dataGridView.Columns.Add(new DataGridViewColumn { Name = "Timestamp", DataPropertyName = "Timestamp", DefaultCellStyle = new DataGridViewCellStyle(cellStyle) { Format = TimeTagBase.DefaultFormat }, ReadOnly = true, Resizable = DataGridViewTriState.True, AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, FillWeight = 30, CellTemplate = new DataGridViewTextBoxCell() }); } private void DataGridView_DataError(object sender, DataGridViewDataErrorEventArgs e) { ShowUpdateMessage($"ERROR: Data grid view exception: {e.Exception?.Message}"); } private void ShowUpdateMessage(string message, bool log = true) { if (m_formClosing) return; if (InvokeRequired) { BeginInvoke(new Action(ShowUpdateMessage), message, log); } else { lock (textBoxMessageOutput) textBoxMessageOutput.AppendText($"{message}{Environment.NewLine}"); if (log) m_log.Publish(MessageLevel.Info, "StatusMessage", message); } } private void ClearUpdateMessages() { if (m_formClosing) return; if (InvokeRequired) { BeginInvoke(new Action(ClearUpdateMessages)); } else { lock (textBoxMessageOutput) textBoxMessageOutput.Text = ""; } } } }