//****************************************************************************************************** // FrameParser.cs - Gbtc // // Copyright © 2012, 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://www.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: // ---------------------------------------------------------------------------------------------------- // 01/14/2005 - J. Ritchie Carroll // Generated original version of source code. // 09/15/2009 - Stephen C. Wills // Added new header and license agreement. // 05/19/2011 - Ritchie // Added DST file support. // 12/17/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.IO; using System.Security.Permissions; using System.Text; using GSF.IO; using GSF.Parsing; namespace GSF.PhasorProtocols.BPAPDCstream { /// /// Represents a frame parser for a BPA PDCstream binary data stream and returns parsed data via events. /// /// /// Frame parser is implemented as a write-only stream - this way data can come from any source. /// [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] // Required by file watcher... public class FrameParser : FrameParserBase { #region [ Members ] // Events /// /// Occurs when a BPA PDCstream has been received. /// /// /// is the that was received. /// public new event EventHandler> ReceivedConfigurationFrame; /// /// Occurs when a BPA PDCstream has been received. /// /// /// is the that was received. /// public new event EventHandler> ReceivedDataFrame; // Fields private ConfigurationFrame m_configurationFrame; private string m_configurationFileName; private bool m_refreshConfigurationFileOnChange; private SafeFileWatcher m_configurationFileWatcher; private readonly object m_syncLock; private bool m_disposed; #endregion #region [ Constructors ] /// /// Creates a new . /// /// Frame types that should perform check-sum validation; default to /// Determines if header lengths should be trusted over parsed byte count. /// The required external BPA PDCstream INI based configuration file. public FrameParser(CheckSumValidationFrameTypes checkSumValidationFrameTypes = CheckSumValidationFrameTypes.AllFrames, bool trustHeaderLength = true, string configurationFileName = null) : base(checkSumValidationFrameTypes, trustHeaderLength) { // Initialize protocol synchronization bytes for this frame parser base.ProtocolSyncBytes = new[] { PhasorProtocols.Common.SyncByte }; m_syncLock = new object(); m_configurationFileName = configurationFileName; } #endregion #region [ Properties ] /// /// Gets or sets current used for parsing 's encountered in the data stream from a device. /// /// /// If a has been parsed, this will return a reference to the parsed frame. Consumer can manually assign a /// to start parsing data if one has not been encountered in the stream. /// public override IConfigurationFrame ConfigurationFrame { get => m_configurationFrame; set => m_configurationFrame = CastToDerivedConfigurationFrame(value, m_configurationFileName); } /// /// Gets flag that determines if this protocol parsing implementation uses synchronization bytes. /// public override bool ProtocolUsesSyncBytes => !UsePhasorDataFileFormat; /// /// Gets or sets required external BPA PDCstream INI based configuration file. /// public string ConfigurationFileName { get => m_configurationFrame?.ConfigurationFileName ?? m_configurationFileName; set { m_configurationFileName = value; ResetFileWatcher(); } } /// /// Gets or sets flag that determines if configuration file is automatically reloaded when it has changed on disk. /// public bool RefreshConfigurationFileOnChange { get => m_refreshConfigurationFileOnChange; set { m_refreshConfigurationFileOnChange = value; ResetFileWatcher(); } } /// /// Gets or sets flag that interprets word count in packet header from a byte instead of a word. /// /// /// Set to true to interpret word count in packet header from a byte instead of a word - if the sync byte /// (0xAA) is at position one, then the word count would be interpreted from byte four. Some older BPA PDC /// stream implementations have a 0x01 in byte three where there should be a 0x00 and this throws off the /// frame length, setting this property to true will correctly interpret the word count. /// public bool ParseWordCountFromByte { get; set; } /// /// Gets or sets flag that determines if source data is in the Phasor Data File Format (i.e., a DST file). /// public bool UsePhasorDataFileFormat { get; set; } /// /// Gets current descriptive status of the . /// public override string Status { get { StringBuilder status = new(); status.Append(" INI configuration file: "); status.Append(FilePath.TrimFileName(m_configurationFileName, 51)); status.AppendLine(); status.Append(" Using phasor file format: "); status.Append(UsePhasorDataFileFormat); status.AppendLine(); if (m_configurationFrame is not null) { status.Append(" BPA PDC stream type: "); status.Append(m_configurationFrame.StreamType); status.AppendLine(); status.Append(" BPA PDC revision number: "); status.Append(m_configurationFrame.RevisionNumber); status.AppendLine(); } status.Append(" Auto-reload configuration: "); status.Append(m_refreshConfigurationFileOnChange); status.AppendLine(); status.Append("Parse word count from byte: "); status.Append(ParseWordCountFromByte); status.AppendLine(); status.Append(base.Status); return status.ToString(); } } /// /// Gets or sets any connection specific that may be needed for parsing. /// public override IConnectionParameters ConnectionParameters { get => base.ConnectionParameters; set { if (value is not ConnectionParameters parameters) return; base.ConnectionParameters = parameters; // Assign new incoming connection parameter values m_configurationFileName = parameters.ConfigurationFileName; ParseWordCountFromByte = parameters.ParseWordCountFromByte; m_refreshConfigurationFileOnChange = parameters.RefreshConfigurationFileOnChange; UsePhasorDataFileFormat = parameters.UsePhasorDataFileFormat; ResetFileWatcher(); } } #endregion #region [ Methods ] /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool disposing) { if (m_disposed) return; try { if (!disposing) return; if (m_configurationFileWatcher is not null) { m_configurationFileWatcher.Changed -= m_configurationFileWatcher_Changed; m_configurationFileWatcher.Dispose(); } m_configurationFileWatcher = null; } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } /// /// Start the data parser. /// public override void Start() { // We narrow down parsing types to just those needed... base.Start(new[] { typeof(DataFrame), typeof(ConfigurationFrame) }); } /// /// Parses a common header instance that implements for the output type represented /// in the binary image. /// /// Buffer containing data to parse. /// Offset index into buffer that represents where to start parsing. /// Maximum length of valid data from offset. /// The which includes a type ID for the to be parsed. /// /// /// Derived classes need to provide a common header instance (i.e., class that implements ) /// for the output types; this will primarily include an ID of the that the data image represents. This parsing is /// only for common header information, actual parsing will be handled by output type via its /// method. This header image should also be used to add needed complex state information about the output type being parsed if needed. /// /// /// If there is not enough buffer available to parse common header (as determined by ), return null. Also, if /// the protocol allows frame length to be determined at the time common header is being parsed and there is not enough buffer to parse /// the entire frame, it will be optimal to prevent further parsing by returning null. /// /// protected override ICommonHeader ParseCommonHeader(byte[] buffer, int offset, int length) { // See if there is enough data in the buffer to parse the common frame header. if (length < CommonFrameHeader.FixedLength) return null; // Parse common frame header CommonFrameHeader parsedFrameHeader = new(ParseWordCountFromByte, UsePhasorDataFileFormat, m_configurationFrame, buffer, offset, length); // As an optimization, we also make sure entire frame buffer image is available to be parsed - by doing this // we eliminate the need to validate length on all subsequent data elements that comprise the frame if (length < parsedFrameHeader.FrameLength) return null; // Expose the frame buffer image in case client needs this data for any reason OnReceivedFrameBufferImage(parsedFrameHeader.FrameType, buffer, offset, parsedFrameHeader.FrameLength); // Handle special parsing states switch (parsedFrameHeader.TypeID) { case FrameType.DataFrame: // Assign data frame parsing state parsedFrameHeader.State = new DataFrameParsingState(parsedFrameHeader.FrameLength, m_configurationFrame, DataCell.CreateNewCell, TrustHeaderLength, ValidateDataFrameCheckSum); break; case FrameType.ConfigurationFrame: // Assign configuration frame parsing state parsedFrameHeader.State = new ConfigurationFrameParsingState(parsedFrameHeader.FrameLength, m_configurationFileName, ConfigurationCell.CreateNewCell, TrustHeaderLength, ValidateConfigurationFrameCheckSum); break; } return parsedFrameHeader; } // Handler for file watcher - we notify consumer when changes have occurred to configuration file private void m_configurationFileWatcher_Changed(object sender, FileSystemEventArgs e) { // We synchronize change actions - don't want more than one refresh happening at a time... lock (m_syncLock) { // Notify consumer of change in configuration OnConfigurationChanged(); // Reload configuration... m_configurationFrame?.Refresh(); } } // Reset file watcher private void ResetFileWatcher() { if (m_configurationFileWatcher is not null) { m_configurationFileWatcher.Changed -= m_configurationFileWatcher_Changed; m_configurationFileWatcher.Dispose(); } m_configurationFileWatcher = null; string configurationFile = ConfigurationFileName; if (!m_refreshConfigurationFileOnChange || string.IsNullOrEmpty(configurationFile) || !File.Exists(configurationFile)) return; try { // Create a new file watcher for configuration file - we'll automatically refresh configuration file when this file gets updated... m_configurationFileWatcher = new SafeFileWatcher(FilePath.GetDirectoryName(configurationFile), FilePath.GetFileName(configurationFile)); m_configurationFileWatcher.Changed += m_configurationFileWatcher_Changed; m_configurationFileWatcher.EnableRaisingEvents = true; m_configurationFileWatcher.IncludeSubdirectories = false; m_configurationFileWatcher.NotifyFilter = NotifyFilters.LastWrite; } catch (Exception ex) { OnParsingException(ex); } } /// /// Raises the event. /// /// to send to event. protected override void OnReceivedConfigurationFrame(IConfigurationFrame frame) { // We override this method so we can cache configuration frame when it's received base.OnReceivedConfigurationFrame(frame); // Cache new configuration frame for parsing subsequent data frames... if (frame is ConfigurationFrame configurationFrame) m_configurationFrame = configurationFrame; } /// /// Casts the parsed to its specific implementation (i.e., or . /// /// that was parsed by that implements protocol specific common frame header interface. protected override void OnReceivedChannelFrame(IChannelFrame frame) { // Raise abstract channel frame events as a priority (i.e., IDataFrame, IConfigurationFrame, etc.) base.OnReceivedChannelFrame(frame); // Raise BPA PDCstream specific channel frame events, if any have been subscribed if (frame is null || ReceivedDataFrame is null && ReceivedConfigurationFrame is null) return; switch (frame) { case DataFrame dataFrame: { ReceivedDataFrame?.Invoke(this, new EventArgs(dataFrame)); break; } case ConfigurationFrame configFrame: { ReceivedConfigurationFrame?.Invoke(this, new EventArgs(configFrame)); break; } } } #endregion #region [ Static ] // Static Methods // Attempts to cast given frame into a BPA PDCstream configuration frame - theoretically this will // allow the same configuration frame to be used for any protocol implementation internal static ConfigurationFrame CastToDerivedConfigurationFrame(IConfigurationFrame sourceFrame, string configurationFileName) { // See if frame is already a BPA PDCstream configuration frame (if so, we don't need to do any work) if (sourceFrame is ConfigurationFrame derivedFrame) return derivedFrame; // Create a new BPA PDCstream configuration frame converted from equivalent configuration information derivedFrame = new ConfigurationFrame(sourceFrame.Timestamp, configurationFileName, 1, RevisionNumber.Revision2, StreamType.Compact); foreach (IConfigurationCell sourceCell in sourceFrame.Cells) { // Create new derived configuration cell ConfigurationCell derivedCell = new(derivedFrame, sourceCell.IDCode, sourceCell.NominalFrequency); // Create equivalent derived phasor definitions foreach (IPhasorDefinition sourcePhasor in sourceCell.PhasorDefinitions) derivedCell.PhasorDefinitions.Add(new PhasorDefinition(derivedCell, sourcePhasor.Label, sourcePhasor.ScalingValue, sourcePhasor.Offset, sourcePhasor.PhasorType, null)); // Create equivalent derived frequency definition IFrequencyDefinition sourceFrequency = sourceCell.FrequencyDefinition; if (sourceFrequency is not null) derivedCell.FrequencyDefinition = new FrequencyDefinition(derivedCell, sourceFrequency.Label); // Create equivalent derived analog definitions (assuming analog type = SinglePointOnWave) foreach (IAnalogDefinition sourceAnalog in sourceCell.AnalogDefinitions) derivedCell.AnalogDefinitions.Add(new AnalogDefinition(derivedCell, sourceAnalog.Label, sourceAnalog.ScalingValue, sourceAnalog.Offset, sourceAnalog.AnalogType)); // Create equivalent derived digital definitions foreach (IDigitalDefinition sourceDigital in sourceCell.DigitalDefinitions) derivedCell.DigitalDefinitions.Add(new DigitalDefinition(derivedCell, sourceDigital.Label)); // Add cell to frame derivedFrame.Cells.Add(derivedCell); } return derivedFrame; } #endregion } }