mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
eb40a59566
Drive the solution to zero warnings / zero errors with analyzers on, promote the consistently-fixed rules to error, and lock in coverage on the only line of new logic.
351 lines
18 KiB
C#
351 lines
18 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Opc.Ua.PubSub.PublishedData
|
|
{
|
|
/// <summary>
|
|
/// Class specialized in collecting published data
|
|
/// </summary>
|
|
public class DataCollector
|
|
{
|
|
private readonly Dictionary<string, PublishedDataSetDataType> m_publishedDataSetsByName;
|
|
private readonly IUaPubSubDataStore m_dataStore;
|
|
private readonly ILogger m_logger;
|
|
|
|
/// <summary>
|
|
/// Create new instance of <see cref="DataCollector"/>.
|
|
/// </summary>
|
|
/// <param name="dataStore">Reference to the <see cref="IUaPubSubDataStore"/> that will be used to collect data.</param>
|
|
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
|
|
public DataCollector(IUaPubSubDataStore dataStore, ITelemetryContext telemetry)
|
|
{
|
|
m_logger = telemetry.CreateLogger<DataCollector>();
|
|
m_dataStore = dataStore;
|
|
m_publishedDataSetsByName = [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a <see cref="PublishedDataSetDataType"/> configuration object.
|
|
/// </summary>
|
|
/// <param name="publishedDataSet">The <see cref="PublishedDataSetDataType"/> that is to be validated.</param>
|
|
/// <returns>true if configuration is correct.</returns>
|
|
/// <exception cref="ArgumentException"><paramref name="publishedDataSet"/></exception>
|
|
public bool ValidatePublishedDataSet(PublishedDataSetDataType publishedDataSet)
|
|
{
|
|
if (publishedDataSet == null)
|
|
{
|
|
throw new ArgumentException(null, nameof(publishedDataSet));
|
|
}
|
|
if (publishedDataSet.DataSetMetaData == null)
|
|
{
|
|
m_logger.LogError("The DataSetMetaData field is null.");
|
|
return false;
|
|
}
|
|
if (ExtensionObject.ToEncodeable(publishedDataSet.DataSetSource)
|
|
is PublishedDataItemsDataType publishedDataItems &&
|
|
publishedDataItems.PublishedData.Count != publishedDataSet.DataSetMetaData.Fields
|
|
.Count)
|
|
{
|
|
m_logger.LogError(
|
|
"The DataSetSource.Count is different from DataSetMetaData.Fields.Count.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a publishedDataSet
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException"><paramref name="publishedDataSet"/></exception>
|
|
public void AddPublishedDataSet(PublishedDataSetDataType publishedDataSet)
|
|
{
|
|
if (publishedDataSet == null)
|
|
{
|
|
throw new ArgumentException(null, nameof(publishedDataSet));
|
|
}
|
|
// validate publishedDataSet
|
|
if (ValidatePublishedDataSet(publishedDataSet))
|
|
{
|
|
// TODO: Consider adding ArgumentNullException.ThrowIfNull(publishedDataSet.Name)
|
|
m_publishedDataSetsByName[publishedDataSet.Name!] = publishedDataSet;
|
|
}
|
|
else
|
|
{
|
|
m_logger.LogError(
|
|
"The PublishedDataSet {Name} was not registered because it is not configured properly.",
|
|
publishedDataSet.Name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove a registered a publishedDataSet
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException"><paramref name="publishedDataSet"/></exception>
|
|
public void RemovePublishedDataSet(PublishedDataSetDataType publishedDataSet)
|
|
{
|
|
if (publishedDataSet == null)
|
|
{
|
|
throw new ArgumentException(null, nameof(publishedDataSet));
|
|
}
|
|
// TODO: Consider adding ArgumentNullException.ThrowIfNull(publishedDataSet.Name)
|
|
m_publishedDataSetsByName.Remove(publishedDataSet.Name!);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create and return a DataSet object created from its dataSetName
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public DataSet? CollectData(string dataSetName)
|
|
{
|
|
PublishedDataSetDataType? publishedDataSet = GetPublishedDataSet(dataSetName);
|
|
|
|
if (publishedDataSet != null)
|
|
{
|
|
m_dataStore.UpdateMetaData(publishedDataSet);
|
|
|
|
if (!publishedDataSet.DataSetSource.IsNull)
|
|
{
|
|
var dataSet = new DataSet(dataSetName)
|
|
{
|
|
DataSetMetaData = publishedDataSet.DataSetMetaData
|
|
};
|
|
|
|
if (ExtensionObject.ToEncodeable(publishedDataSet.DataSetSource)
|
|
is PublishedDataItemsDataType publishedDataItems &&
|
|
!publishedDataItems.PublishedData.IsEmpty)
|
|
{
|
|
dataSet.Fields = new Field[publishedDataItems.PublishedData.Count];
|
|
for (int i = 0; i < publishedDataItems.PublishedData.Count; i++)
|
|
{
|
|
try
|
|
{
|
|
PublishedVariableDataType publishedVariable = publishedDataItems
|
|
.PublishedData[i];
|
|
dataSet.Fields[i] = new Field
|
|
{
|
|
// set FieldMetaData property
|
|
FieldMetaData = publishedDataSet.DataSetMetaData.Fields[i]
|
|
};
|
|
|
|
// retrieve value from DataStore
|
|
DataValue dataValue = default;
|
|
|
|
if (!publishedVariable.PublishedVariable.IsNull)
|
|
{
|
|
m_dataStore.TryReadPublishedDataItem(
|
|
publishedVariable.PublishedVariable,
|
|
publishedVariable.AttributeId,
|
|
out dataValue);
|
|
}
|
|
|
|
if (dataValue.IsNull)
|
|
{
|
|
//try to get the dataValue from ExtensionFields
|
|
/*If an entry of the PublishedData references one of the ExtensionFields, the substituteValue shall contain the
|
|
* QualifiedName of the ExtensionFields entry.
|
|
* All other fields of this PublishedVariableDataType array element shall be null*/
|
|
if (publishedVariable.SubstituteValue.TryGetValue(out QualifiedName extensionFieldName))
|
|
{
|
|
KeyValuePair extensionField = publishedDataSet
|
|
.ExtensionFields
|
|
.Find(x =>
|
|
x.Key == extensionFieldName);
|
|
if (!extensionField.Key.IsNull)
|
|
{
|
|
dataValue = new DataValue(extensionField.Value);
|
|
}
|
|
}
|
|
if (dataValue.IsNull)
|
|
{
|
|
dataValue = DataValue.FromStatusCode(StatusCodes.Bad, DateTime.UtcNow);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dataValue = dataValue.Copy();
|
|
|
|
//check StatusCode and return SubstituteValue if possible
|
|
if (dataValue.StatusCode == StatusCodes.Bad &&
|
|
publishedVariable.SubstituteValue != Variant.Null)
|
|
{
|
|
dataValue = dataValue
|
|
.WithWrappedValue(publishedVariable.SubstituteValue)
|
|
.WithStatus(StatusCodes.UncertainSubstituteValue);
|
|
}
|
|
}
|
|
|
|
dataValue = dataValue.WithServerTimestamp(DateTimeUtc.Now);
|
|
|
|
Field field = dataSet.Fields[i];
|
|
Variant variant = dataValue.WrappedValue;
|
|
|
|
bool ShouldBringToConstraints(uint givenStrlen)
|
|
{
|
|
return field.FieldMetaData!.MaxStringLength > 0 &&
|
|
givenStrlen > field.FieldMetaData.MaxStringLength;
|
|
}
|
|
|
|
var builtInType = (BuiltInType)field.FieldMetaData!.BuiltInType;
|
|
switch (builtInType)
|
|
{
|
|
case BuiltInType.String:
|
|
if (field.FieldMetaData.ValueRank == ValueRanks.Scalar)
|
|
{
|
|
if (variant.TryGetValue(out string strFieldValue) &&
|
|
ShouldBringToConstraints(
|
|
(uint)strFieldValue.Length))
|
|
{
|
|
dataValue = dataValue.WithWrappedValue(Variant.From(
|
|
strFieldValue[..(int)field.FieldMetaData.MaxStringLength]));
|
|
}
|
|
}
|
|
else if (field.FieldMetaData.ValueRank == ValueRanks.OneDimension)
|
|
{
|
|
if (variant.TryGetValue(out ArrayOf<string> valueArray))
|
|
{
|
|
string[] buffer = new string[valueArray.Count];
|
|
for (int idx = 0; idx < valueArray.Count; idx++)
|
|
{
|
|
if (ShouldBringToConstraints(
|
|
(uint)valueArray[idx].Length))
|
|
{
|
|
buffer[idx] = valueArray[idx]
|
|
[..(int)field.FieldMetaData.MaxStringLength];
|
|
}
|
|
else
|
|
{
|
|
buffer[idx] = valueArray[idx];
|
|
}
|
|
}
|
|
dataValue = dataValue.WithWrappedValue(
|
|
Variant.From(buffer.ToArrayOf()));
|
|
}
|
|
else
|
|
{
|
|
dataValue = dataValue.WithWrappedValue(default);
|
|
}
|
|
}
|
|
break;
|
|
case BuiltInType.ByteString:
|
|
if (field.FieldMetaData.ValueRank == ValueRanks.Scalar)
|
|
{
|
|
if (variant.TryGetValue(out ByteString byteStringFieldValue) &&
|
|
ShouldBringToConstraints((uint)byteStringFieldValue.Length))
|
|
{
|
|
byte[] byteArray = byteStringFieldValue.ToArray();
|
|
Array.Resize(
|
|
ref byteArray,
|
|
(int)field.FieldMetaData.MaxStringLength);
|
|
dataValue = dataValue.WithWrappedValue(
|
|
Variant.From(ByteString.From(byteArray)));
|
|
}
|
|
}
|
|
else if (field.FieldMetaData.ValueRank == ValueRanks.OneDimension)
|
|
{
|
|
if (variant.TryGetValue(out ArrayOf<ByteString> valueArray))
|
|
{
|
|
var buffer = new ByteString[valueArray.Count];
|
|
for (int idx = 0; idx < valueArray.Count; idx++)
|
|
{
|
|
if (ShouldBringToConstraints((uint)valueArray[idx].Length))
|
|
{
|
|
byte[] byteArray = valueArray[idx].ToArray();
|
|
Array.Resize(
|
|
ref byteArray,
|
|
(int)field.FieldMetaData
|
|
.MaxStringLength);
|
|
buffer[idx] = ByteString.From(byteArray);
|
|
}
|
|
else
|
|
{
|
|
buffer[idx] = valueArray[idx];
|
|
}
|
|
}
|
|
valueArray = buffer.ToArrayOf();
|
|
dataValue = dataValue.WithWrappedValue(Variant.From(valueArray));
|
|
}
|
|
else
|
|
{
|
|
dataValue = dataValue.WithWrappedValue(default);
|
|
}
|
|
}
|
|
break;
|
|
case >= BuiltInType.Null and <= BuiltInType.Enumeration:
|
|
break;
|
|
default:
|
|
throw ServiceResultException.Unexpected(
|
|
$"Unexpected BuiltInType {builtInType}");
|
|
}
|
|
|
|
dataSet.Fields[i].Value = dataValue;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
dataSet.Fields[i].Value
|
|
= DataValue.FromStatusCode(StatusCodes.Bad, DateTime.UtcNow);
|
|
m_logger.LogInformation(ex,
|
|
"Error DataCollector.CollectData for dataset {Name} field {Index}",
|
|
dataSetName,
|
|
i);
|
|
}
|
|
}
|
|
return dataSet;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get The <see cref="PublishedDataSetDataType"/> for a DataSetName
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException"><paramref name="dataSetName"/></exception>
|
|
public PublishedDataSetDataType? GetPublishedDataSet(string dataSetName)
|
|
{
|
|
if (dataSetName == null)
|
|
{
|
|
throw new ArgumentException(null, nameof(dataSetName));
|
|
}
|
|
|
|
if (m_publishedDataSetsByName.TryGetValue(
|
|
dataSetName,
|
|
out PublishedDataSetDataType? value))
|
|
{
|
|
return value;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|