/* ======================================================================== * 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 { /// /// Class specialized in collecting published data /// public class DataCollector { private readonly Dictionary m_publishedDataSetsByName; private readonly IUaPubSubDataStore m_dataStore; private readonly ILogger m_logger; /// /// Create new instance of . /// /// Reference to the that will be used to collect data. /// The telemetry context to use to create obvservability instruments public DataCollector(IUaPubSubDataStore dataStore, ITelemetryContext telemetry) { m_logger = telemetry.CreateLogger(); m_dataStore = dataStore; m_publishedDataSetsByName = []; } /// /// Validates a configuration object. /// /// The that is to be validated. /// true if configuration is correct. /// 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; } /// /// Register a publishedDataSet /// /// 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); } } /// /// Remove a registered a publishedDataSet /// /// 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!); } /// /// Create and return a DataSet object created from its dataSetName /// /// 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 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 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; } /// /// Get The for a DataSetName /// /// 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; } } }