generated from gemstone/gemtem
-
Notifications
You must be signed in to change notification settings - Fork 0
/
FrameParser.cs
608 lines (529 loc) · 27.4 KB
/
FrameParser.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
//******************************************************************************************************
// 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:
// ----------------------------------------------------------------------------------------------------
// 04/30/2009 - J. Ritchie Carroll
// Generated original version of source code.
// 09/15/2009 - Stephen C. Wills
// Added new header and license agreement.
// 12/17/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.IO;
using System.Text;
using System.Threading;
using Gemstone.ArrayExtensions;
using Gemstone.EventHandlerExtensions;
using Gemstone.IO;
using Gemstone.IO.Parsing;
using Gemstone.StringExtensions;
namespace Gemstone.PhasorProtocols.Macrodyne
{
/// <summary>
/// Represents a frame parser for a Macrodyne binary data stream that returns parsed data via events.
/// </summary>
/// <remarks>
/// Frame parser is implemented as a write-only stream - this way data can come from any source.
/// </remarks>
public class FrameParser : FrameParserBase<FrameType>
{
#region [ Members ]
// Events
/// <summary>
/// Occurs when a Macrodyne <see cref="HeaderFrame"/> that contains the UnitID (i.e., station name) has been received.
/// </summary>
/// <remarks>
/// <see cref="EventArgs{T}.Argument"/> is the <see cref="HeaderFrame"/> that was received.
/// </remarks>
public new event EventHandler<EventArgs<HeaderFrame>>? ReceivedHeaderFrame;
/// <summary>
/// Occurs when a Macrodyne <see cref="ConfigurationFrame"/> has been received.
/// </summary>
/// <remarks>
/// <see cref="EventArgs{T}.Argument"/> is the <see cref="ConfigurationFrame"/> that was received.
/// </remarks>
public new event EventHandler<EventArgs<ConfigurationFrame>>? ReceivedConfigurationFrame;
/// <summary>
/// Occurs when a Macrodyne <see cref="DataFrame"/> has been received.
/// </summary>
/// <remarks>
/// <see cref="EventArgs{T}.Argument"/> is the <see cref="DataFrame"/> that was received.
/// </remarks>
public new event EventHandler<EventArgs<DataFrame>>? ReceivedDataFrame;
// Fields
private HeaderFrame m_headerFrame;
private ConfigurationFrame m_configurationFrame;
private ProtocolVersion m_protocolVersion;
private string m_configurationFileName;
private bool m_refreshConfigurationFileOnChange;
private SafeFileWatcher m_configurationFileWatcher;
private readonly object m_syncLock;
private bool m_disposed;
#endregion
#region [ Constructors ]
/// <summary>
/// Creates a new <see cref="FrameParser"/> from specified parameters.
/// </summary>
/// <param name="checkSumValidationFrameTypes">Frame types that should perform check-sum validation; default to <see cref="CheckSumValidationFrameTypes.AllFrames"/></param>
/// <param name="trustHeaderLength">Determines if header lengths should be trusted over parsed byte count.</param>
/// <param name="protocolVersion">The protocol version that the parser should use.</param>
/// <param name="configurationFileName">The optional external Macrodyne configuration in BPA PDCstream INI file based format.</param>
/// <param name="deviceLabel">The INI section device label to use.</param>
public FrameParser(CheckSumValidationFrameTypes checkSumValidationFrameTypes = CheckSumValidationFrameTypes.AllFrames, bool trustHeaderLength = true, ProtocolVersion protocolVersion = ProtocolVersion.M, string configurationFileName = null, string deviceLabel = null)
: base(checkSumValidationFrameTypes, trustHeaderLength)
{
m_syncLock = new object();
m_protocolVersion = protocolVersion;
DeviceLabel = deviceLabel;
ConfigurationFileName = configurationFileName;
}
#endregion
#region [ Properties ]
/// <summary>
/// Gets or sets current <see cref="IConfigurationFrame"/> used for parsing <see cref="IDataFrame"/>'s encountered in the data stream from a device.
/// </summary>
/// <remarks>
/// If a <see cref="IConfigurationFrame"/> has been parsed, this will return a reference to the parsed frame. Consumer can manually assign a
/// <see cref="IConfigurationFrame"/> to start parsing data if one has not been encountered in the stream.
/// </remarks>
public override IConfigurationFrame ConfigurationFrame
{
get => m_configurationFrame;
set => m_configurationFrame = CastToDerivedConfigurationFrame(value, m_configurationFileName, DeviceLabel);
}
/// <summary>
/// Gets flag that determines if Macrodyne protocol parsing implementation uses synchronization bytes.
/// </summary>
public override bool ProtocolUsesSyncBytes => false;
/// <summary>
/// Gets or sets external Macrodyne based configuration file.
/// </summary>
public string ConfigurationFileName
{
get
{
if (m_configurationFrame is null)
return m_configurationFileName;
return m_configurationFrame.ConfigurationFileName;
}
set
{
m_configurationFileName = value;
ResetFileWatcher();
if (m_configurationFrame is null && !string.IsNullOrEmpty(m_configurationFileName) && File.Exists(m_configurationFileName))
{
m_configurationFrame = new ConfigurationFrame(OnlineDataFormatFlags.TimestampEnabled, "1690", m_configurationFileName, DeviceLabel)
{
CommonHeader = new CommonFrameHeader(m_protocolVersion, FrameType.ConfigurationFrame)
};
}
}
}
/// <summary>
/// Gets or sets device section label, as defined in associated INI file.
/// </summary>
public string DeviceLabel { get; set; }
/// <summary>
/// Gets or sets flag that determines if configuration file is automatically reloaded when it has changed on disk.
/// </summary>
public bool RefreshConfigurationFileOnChange
{
get => m_refreshConfigurationFileOnChange;
set
{
m_refreshConfigurationFileOnChange = value;
ResetFileWatcher();
}
}
/// <summary>
/// Gets current descriptive status of the <see cref="FrameParser"/>.
/// </summary>
public override string Status
{
get
{
StringBuilder status = new();
status.AppendFormat("Macrodyne protocol version: {0}", m_protocolVersion);
status.AppendLine();
status.AppendFormat(" INI configuration file: {0}", FilePath.TrimFileName(m_configurationFileName.ToNonNullNorEmptyString("undefined"), 51));
status.AppendLine();
status.AppendFormat(" Device INI section label: {0}", DeviceLabel);
status.AppendLine();
status.AppendFormat(" Auto-reload configuration: {0}", m_refreshConfigurationFileOnChange);
status.AppendLine();
status.Append(base.Status);
return status.ToString();
}
}
/// <summary>
/// Gets or sets any connection specific <see cref="IConnectionParameters"/> that may be needed for parsing.
/// </summary>
public override IConnectionParameters ConnectionParameters
{
get => base.ConnectionParameters;
set
{
if (value is ConnectionParameters parameters)
{
base.ConnectionParameters = parameters;
// Assign new incoming connection parameter values
m_protocolVersion = parameters.ProtocolVersion;
DeviceLabel = parameters.DeviceLabel;
ConfigurationFileName = parameters.ConfigurationFileName;
m_refreshConfigurationFileOnChange = parameters.RefreshConfigurationFileOnChange;
ResetFileWatcher();
}
}
}
#endregion
#region [ Methods ]
/// <summary>
/// Releases the unmanaged resources used by the <see cref="FrameParser"/> object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
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().
}
}
/// <summary>
/// Start the data parser.
/// </summary>
public override void Start()
{
// We narrow down parsing types to just those needed...
base.Start(new[] { typeof(DataFrame), typeof(HeaderFrame), typeof(ConfigurationFrame) });
// Make sure we mark stream an initialized even though base class doesn't think we use sync-bytes
StreamInitialized = false;
try
{
// Publish configuration frame
ThreadPool.QueueUserWorkItem(PublishConfigurationFrame);
}
catch (Exception ex)
{
// Process exception for logging
OnParsingException(new InvalidOperationException($"Failed to publish configuration frame due to exception: {ex.Message}", ex));
}
}
/// <summary>
/// Parses a common header instance that implements <see cref="ICommonHeader{TTypeIdentifier}"/> for the output type represented
/// in the binary image.
/// </summary>
/// <param name="buffer">Buffer containing data to parse.</param>
/// <param name="offset">Offset index into buffer that represents where to start parsing.</param>
/// <param name="length">Maximum length of valid data from offset.</param>
/// <returns>The <see cref="ICommonHeader{TTypeIdentifier}"/> which includes a type ID for the <see cref="Type"/> to be parsed.</returns>
/// <remarks>
/// <para>
/// Derived classes need to provide a common header instance (i.e., class that implements <see cref="ICommonHeader{TTypeIdentifier}"/>)
/// for the output types; this will primarily include an ID of the <see cref="Type"/> that the data image represents. This parsing is
/// only for common header information, actual parsing will be handled by output type via its <see cref="ISupportBinaryImage.ParseBinaryImage"/>
/// method. This header image should also be used to add needed complex state information about the output type being parsed if needed.
/// </para>
/// <para>
/// If there is not enough buffer available to parse common header (as determined by <paramref name="length"/>), 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.
/// </para>
/// </remarks>
protected override ICommonHeader<FrameType> 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)
{
// Parse common frame header
CommonFrameHeader parsedFrameHeader = new(buffer, offset, m_protocolVersion, m_configurationFrame);
// 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)
{
// Expose the frame buffer image in case client needs this data for any reason
OnReceivedFrameBufferImage(parsedFrameHeader.FrameType, buffer, offset, length);
// 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.HeaderFrame:
// Assign header frame parsing state
parsedFrameHeader.State = new HeaderFrameParsingState(parsedFrameHeader.FrameLength, parsedFrameHeader.DataLength, TrustHeaderLength, ValidateHeaderFrameCheckSum);
break;
case FrameType.ConfigurationFrame:
// Assign configuration frame parsing state
parsedFrameHeader.State = new ConfigurationFrameParsingState(parsedFrameHeader.FrameLength, m_headerFrame, ConfigurationCell.CreateNewCell, TrustHeaderLength, ValidateConfigurationFrameCheckSum);
break;
}
return parsedFrameHeader;
}
}
return null;
}
/// <summary>
/// Writes a sequence of bytes onto the stream for parsing.
/// </summary>
/// <param name="source">Defines the source channel for the data.</param>
/// <param name="buffer">An array of bytes. This method copies count bytes from buffer to the current stream.</param>
/// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
/// <param name="count">The number of bytes to be written to the current stream.</param>
public override void Parse(SourceChannel source, byte[] buffer, int offset, int count)
{
// Since the Macrodyne implementation supports both 0xAA and 0xBB as sync-bytes, we must manually check for both during stream initialization,
// base class handles this only then there is a consistently defined set of sync-bytes, not variable.
if (Enabled)
{
// See if there are any 0xAA 0xAA sequences - these must be removed
int syncBytePosition = buffer.IndexOfSequence(new byte[] { 0xAA, 0xAA }, offset, count);
while (syncBytePosition > -1)
{
using (BlockAllocatedMemoryStream newBuffer = new())
{
// Write buffer before repeated byte
newBuffer.Write(buffer, offset, syncBytePosition - offset + 1);
int nextByte = syncBytePosition + 2;
// Write buffer after repeated byte, if any
if (nextByte < offset + count)
newBuffer.Write(buffer, nextByte, offset + count - nextByte);
buffer = newBuffer.ToArray();
}
offset = 0;
count = buffer.Length;
// Find next 0xAA 0xAA sequence
syncBytePosition = buffer.IndexOfSequence(new byte[] { 0xAA, 0xAA }, offset, count);
}
if (StreamInitialized)
{
base.Parse(source, buffer, offset, count);
}
else
{
// Initial stream may be anywhere in the middle of a frame, so we attempt to locate sync-bytes to "line-up" data stream,
// First we look for data frame sync-byte:
syncBytePosition = buffer.IndexOfSequence(new byte[] { 0xAA }, offset, count);
if (syncBytePosition > -1)
{
StreamInitialized = true;
base.Parse(source, buffer, syncBytePosition, count - (syncBytePosition - offset));
}
else
{
// Second we look for command frame response sync-byte:
syncBytePosition = buffer.IndexOfSequence(new byte[] { 0xBB }, offset, count);
if (syncBytePosition > -1)
{
StreamInitialized = true;
base.Parse(source, buffer, syncBytePosition, count - (syncBytePosition - offset));
}
}
}
}
}
// Publish the current configuration frame
private void PublishConfigurationFrame(object state)
{
try
{
if (m_configurationFrame is not null)
OnReceivedConfigurationFrame(m_configurationFrame);
}
catch (ThreadAbortException)
{
throw;
}
catch (Exception ex)
{
OnParsingException(ex);
}
}
// 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))
{
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);
}
}
}
/// <summary>
/// Raises the <see cref="FrameParserBase{TypeIndentifier}.ReceivedConfigurationFrame"/> event.
/// </summary>
/// <param name="frame"><see cref="IConfigurationFrame"/> to send to <see cref="FrameParserBase{TypeIndentifier}.ReceivedConfigurationFrame"/> event.</param>
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;
}
/// <summary>
/// Raises the <see cref="FrameParserBase{TypeIndentifier}.ReceivedHeaderFrame"/> event.
/// </summary>
/// <param name="frame"><see cref="IHeaderFrame"/> to send to <see cref="FrameParserBase{TypeIndentifier}.ReceivedHeaderFrame"/> event.</param>
protected override void OnReceivedHeaderFrame(IHeaderFrame frame)
{
// We override this method so we can cache header frame when it's received
base.OnReceivedHeaderFrame(frame);
//// Cache new header frame for parsing subsequent configuration frame...
if (frame is HeaderFrame headerFrame)
m_headerFrame = headerFrame;
}
/// <summary>
/// Casts the parsed <see cref="IChannelFrame"/> to its specific implementation (i.e., <see cref="DataFrame"/> or <see cref="ConfigurationFrame"/>).
/// </summary>
/// <param name="frame"><see cref="IChannelFrame"/> that was parsed by <see cref="FrameImageParserBase{TTypeIdentifier,TOutputType}"/> that implements protocol specific common frame header interface.</param>
protected override void OnReceivedChannelFrame(IChannelFrame frame)
{
// Raise abstract channel frame events as a priority (i.e., IDataFrame, IConfigurationFrame, etc.)
base.OnReceivedChannelFrame(frame);
// Raise Macrodyne specific channel frame events, if any have been subscribed
if (frame is null || ReceivedDataFrame is null && ReceivedConfigurationFrame is null && ReceivedHeaderFrame is null)
return;
switch (frame)
{
case DataFrame dataFrame:
{
ReceivedDataFrame?.SafeInvoke(this, new EventArgs<DataFrame>(dataFrame));
break;
}
case HeaderFrame headerFrame:
{
ReceivedHeaderFrame?.SafeInvoke(this, new EventArgs<HeaderFrame>(headerFrame));
break;
}
case ConfigurationFrame configFrame:
{
ReceivedConfigurationFrame?.SafeInvoke(this, new EventArgs<ConfigurationFrame>(configFrame));
break;
}
}
}
/// <summary>
/// Raises the <see cref="BinaryImageParserBase.ParsingException"/> event.
/// </summary>
/// <param name="ex">The <see cref="Exception"/> that was encountered during parsing.</param>
protected override void OnParsingException(Exception ex)
{
base.OnParsingException(ex);
// At the first sign of an error, we need to reset stream initialization flag - could just be looping a saved file source or we missed some data,
// just need to resync to next 0xAA byte...
StreamInitialized = false;
}
#endregion
#region [ Static ]
// Attempts to cast given frame into a Macrodyne 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, string deviceLabel)
{
// See if frame is already a Macrodyne frame (if so, we don't need to do any work)
ConfigurationFrame derivedFrame = sourceFrame as ConfigurationFrame;
if (derivedFrame is null)
{
// Create a new Macrodyne configuration frame converted from equivalent configuration information; Macrodyne only supports one device
if (sourceFrame.Cells.Count > 0)
{
IConfigurationCell sourceCell = sourceFrame.Cells[0];
string stationName = sourceCell.StationName;
if (string.IsNullOrEmpty(stationName))
stationName = $"Unit {sourceCell.IDCode}";
stationName = stationName.TruncateLeft(8);
derivedFrame = new ConfigurationFrame(Common.GetFormatFlagsFromPhasorCount(sourceFrame.Cells[0].PhasorDefinitions.Count), stationName, configurationFileName, deviceLabel)
{
IDCode = sourceFrame.IDCode
};
// Create new derived configuration cell
ConfigurationCell derivedCell = new(derivedFrame);
IFrequencyDefinition sourceFrequency;
// Create equivalent derived phasor definitions
foreach (IPhasorDefinition sourcePhasor in sourceCell.PhasorDefinitions)
{
derivedCell.PhasorDefinitions.Add(new PhasorDefinition(derivedCell, sourcePhasor.Label, sourcePhasor.PhasorType, null));
}
// Create equivalent derived frequency definition
sourceFrequency = sourceCell.FrequencyDefinition;
if (sourceFrequency is not null)
{
derivedCell.FrequencyDefinition = new FrequencyDefinition(derivedCell)
{
Label = sourceFrequency.Label
};
}
// 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
}
}