/* ======================================================================== * 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 System.Threading; using Microsoft.Extensions.Logging; using Opc.Ua; using Opc.Ua.PubSub; namespace Quickstarts.ConsoleReferencePublisher { internal sealed class PublishedValuesWrites : IDisposable { /// /// It should match the namespace index from configuration file /// public const ushort NamespaceIndexSimple = 2; public const ushort NamespaceIndexAllTypes = 3; private const string kDataSetNameSimple = "Simple"; private const string kDataSetNameAllTypes = "AllTypes"; /// /// simulate for BoolToogle changes to 3 seconds /// private int m_boolToogleCount; private const int kBoolToogleLimit = 2; private const int kSimpleInt32Limit = 10000; private readonly List m_simpleFields = []; private readonly List m_allTypesFields = []; private readonly ILogger m_logger; private readonly ArrayOf m_publishedDataSets; private readonly IUaPubSubDataStore m_dataStore; private readonly TimeProvider m_timeProvider; private ITimer m_updateValuesTimer = null!; private readonly string[] m_aviationAlphabet = [ "Alfa", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", "November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", "Tango", "Uniform", "Victor", "Whiskey", "X-Ray", "Yankee", "Zulu" ]; private int m_aviationAlphabetIndex; private readonly Lock m_lock = new(); /// /// Constructor /// /// /// /// /// Optional time provider. Defaults to . /// public PublishedValuesWrites( UaPubSubApplication uaPubSubApplication, ITelemetryContext telemetry, TimeProvider? timeProvider = null) { m_logger = telemetry.CreateLogger(); m_publishedDataSets = uaPubSubApplication.UaPubSubConfigurator.PubSubConfiguration .PublishedDataSets; m_dataStore = uaPubSubApplication.DataStore; m_timeProvider = timeProvider ?? TimeProvider.System; } public void Dispose() { m_updateValuesTimer.Dispose(); } /// /// Initialize PublisherData with information from configuration and start timer to update data /// public void Start() { if (!m_publishedDataSets.IsNull) { // Remember the fields to be updated foreach (PublishedDataSetDataType publishedDataSet in m_publishedDataSets) { switch (publishedDataSet.Name) { case kDataSetNameSimple: m_simpleFields.AddRange(publishedDataSet.DataSetMetaData.Fields); break; case kDataSetNameAllTypes: m_allTypesFields.AddRange(publishedDataSet.DataSetMetaData.Fields); break; default: m_logger.LogInformation( "PublishedValuesWrites.Start: {DataSet} unknown.", publishedDataSet.Name); break; } } } try { LoadInitialData(); } catch (Exception e) { m_logger.LogError(e, "SamplePublisher.DataStoreValuesGenerator.LoadInitialData wrong field"); } m_updateValuesTimer = m_timeProvider.CreateTimer( UpdateValues, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); } /// /// Load initial demo data /// private void LoadInitialData() { WriteFieldData( "BoolToggle", NamespaceIndexSimple, new DataValue(new Variant(false), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "Int32", NamespaceIndexSimple, new DataValue(new Variant(0), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "Int32Fast", NamespaceIndexSimple, new DataValue(new Variant(0), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "DateTime", NamespaceIndexSimple, new DataValue(new Variant(DateTime.UtcNow), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "BoolToggle", NamespaceIndexAllTypes, new DataValue(new Variant(true), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "Byte", NamespaceIndexAllTypes, new DataValue(new Variant((byte)0), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "Int16", NamespaceIndexAllTypes, new DataValue(new Variant((short)0), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "Int32", NamespaceIndexAllTypes, new DataValue(new Variant(0), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "SByte", NamespaceIndexAllTypes, new DataValue(new Variant((sbyte)0), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "UInt16", NamespaceIndexAllTypes, new DataValue(new Variant((ushort)0), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "UInt32", NamespaceIndexAllTypes, new DataValue(new Variant((uint)0), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "UInt64", NamespaceIndexAllTypes, new DataValue(new Variant((ulong)0), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "Float", NamespaceIndexAllTypes, new DataValue(new Variant((float)0F), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "Double", NamespaceIndexAllTypes, new DataValue(new Variant((double)0.0), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "String", NamespaceIndexAllTypes, new DataValue(new Variant(m_aviationAlphabet[0]), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "ByteString", NamespaceIndexAllTypes, new DataValue( new Variant(ByteString.From([1, 2, 3])), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "Guid", NamespaceIndexAllTypes, new DataValue(new Variant(Uuid.NewUuid()), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "DateTime", NamespaceIndexAllTypes, new DataValue(new Variant(DateTime.UtcNow), StatusCodes.Good, DateTime.UtcNow) ); WriteFieldData( "UInt32Array", NamespaceIndexAllTypes, new DataValue( new Variant(new uint[] { 1, 2, 3 }), StatusCodes.Good, DateTime.UtcNow) ); } /// /// Write (update) field data /// /// /// private void WriteFieldData( string metaDatafieldName, ushort namespaceIndex, DataValue dataValue) { m_dataStore.WritePublishedDataItem( new NodeId(metaDatafieldName, namespaceIndex), Attributes.Value, dataValue ); } /// /// Simulate value changes in dynamic nodes /// /// private void UpdateValues(object? state) { try { lock (m_lock) { foreach (FieldMetaData variable in m_simpleFields) { switch (variable.Name) { case "BoolToggle": m_boolToogleCount++; if (m_boolToogleCount >= kBoolToogleLimit) { m_boolToogleCount = 0; IncrementValue(variable, NamespaceIndexSimple); } break; case "Int32": IncrementValue(variable, NamespaceIndexSimple, kSimpleInt32Limit); break; case "Int32Fast": IncrementValue( variable, NamespaceIndexSimple, kSimpleInt32Limit, 100); break; case "DateTime": IncrementValue(variable, NamespaceIndexSimple); break; default: m_logger.LogDebug("{Variable} not processed.", variable.Name); break; } } foreach (FieldMetaData variable in m_allTypesFields) { IncrementValue(variable, NamespaceIndexAllTypes); } } } catch (Exception e) { m_logger.LogError(e, "Unexpected error doing simulation."); } } /// /// Increment value /// maxAllowedValue - maximum incremented value before reset value to initial value /// step - the increment value /// /// /// /// /// /// private void IncrementValue( FieldMetaData variable, ushort namespaceIndex, long maxAllowedValue = int.MaxValue, int step = 0 ) { // Read value to be incremented if (!m_dataStore.TryReadPublishedDataItem( new NodeId(variable.Name!, namespaceIndex), Attributes.Value, out DataValue dataValue) || dataValue.WrappedValue.IsNull) { return; } bool valueUpdated = false; BuiltInType builtInType = TypeInfo.GetBuiltInType(variable.DataType); switch (builtInType) { case BuiltInType.Boolean: if (variable.ValueRank == ValueRanks.Scalar) { bool boolValue = (bool)dataValue.WrappedValue.ConvertToBoolean(); dataValue = dataValue.WithWrappedValue(!boolValue); valueUpdated = true; } break; case BuiltInType.Byte: if (variable.ValueRank == ValueRanks.Scalar) { byte byteValue = (byte)dataValue.WrappedValue.ConvertToByte(); dataValue = dataValue.WithWrappedValue((byte)(byteValue + 1)); valueUpdated = true; } break; case BuiltInType.Int16: if (variable.ValueRank == ValueRanks.Scalar) { int intIdentifier = (short)dataValue.WrappedValue.ConvertToInt16(); Interlocked.CompareExchange(ref intIdentifier, 0, short.MaxValue); dataValue = dataValue.WithWrappedValue((short)Interlocked.Increment(ref intIdentifier)); valueUpdated = true; } break; case BuiltInType.Int32: if (variable.ValueRank == ValueRanks.Scalar) { int int32Value = (int)dataValue.WrappedValue.ConvertToInt32(); if (step > 0) { int32Value += step - 1; } if (int32Value > maxAllowedValue) { int32Value = 0; } dataValue = dataValue.WithWrappedValue(Interlocked.Increment(ref int32Value)); valueUpdated = true; } break; case BuiltInType.SByte: if (variable.ValueRank == ValueRanks.Scalar) { int intIdentifier = (sbyte)dataValue.WrappedValue.ConvertToSByte(); Interlocked.CompareExchange(ref intIdentifier, 0, sbyte.MaxValue); dataValue = dataValue.WithWrappedValue((sbyte)Interlocked.Increment(ref intIdentifier)); valueUpdated = true; } break; case BuiltInType.UInt16: if (variable.ValueRank == ValueRanks.Scalar) { int intIdentifier = dataValue.WrappedValue.ConvertToUInt16().GetUInt16(); Interlocked.CompareExchange(ref intIdentifier, 0, ushort.MaxValue); dataValue = dataValue.WithWrappedValue((ushort)Interlocked.Increment(ref intIdentifier)); valueUpdated = true; } break; case BuiltInType.UInt32: if (variable.ValueRank == ValueRanks.Scalar) { long longIdentifier = dataValue.WrappedValue.ConvertToUInt32().GetUInt32(); Interlocked.CompareExchange(ref longIdentifier, 0, uint.MaxValue); dataValue = dataValue.WithWrappedValue((uint)Interlocked.Increment(ref longIdentifier)); valueUpdated = true; } else if (variable.ValueRank == ValueRanks.OneDimension) { if (dataValue.WrappedValue.TryGetValue(out ArrayOf values)) { var valuesList = values.ToList(); for (int i = 0; i < values.Count; i++) { long longIdentifier = values[i]; Interlocked.CompareExchange(ref longIdentifier, 0, uint.MaxValue); valuesList[i] = (uint)Interlocked.Increment(ref longIdentifier); } dataValue = dataValue.WithWrappedValue(valuesList.ToArrayOf()); valueUpdated = true; } } break; case BuiltInType.UInt64: if (variable.ValueRank == ValueRanks.Scalar) { ulong uint64Value = (ulong)dataValue.WrappedValue.ConvertToUInt64(); float longIdentifier = uint64Value + 1; Interlocked.CompareExchange(ref longIdentifier, 0, ulong.MaxValue); dataValue = dataValue.WithWrappedValue((ulong)longIdentifier); valueUpdated = true; } break; case BuiltInType.Float: if (variable.ValueRank == ValueRanks.Scalar) { float floatValue = (float)dataValue.WrappedValue.ConvertToFloat(); Interlocked.CompareExchange(ref floatValue, 0, float.MaxValue); dataValue = dataValue.WithWrappedValue(floatValue + 1); valueUpdated = true; } break; case BuiltInType.Double: if (variable.ValueRank == ValueRanks.Scalar) { double doubleValue = (double)dataValue.WrappedValue.ConvertToDouble(); Interlocked.CompareExchange(ref doubleValue, 0, double.MaxValue); dataValue = dataValue.WithWrappedValue(doubleValue + 1); valueUpdated = true; } break; case BuiltInType.DateTime: if (variable.ValueRank == ValueRanks.Scalar) { dataValue = dataValue.WithWrappedValue(DateTimeUtc.Now); valueUpdated = true; } break; case BuiltInType.Guid: if (variable.ValueRank == ValueRanks.Scalar) { dataValue = dataValue.WithWrappedValue(Uuid.NewUuid()); valueUpdated = true; } break; case BuiltInType.String: if (variable.ValueRank == ValueRanks.Scalar) { m_aviationAlphabetIndex = (m_aviationAlphabetIndex + 1) % m_aviationAlphabet.Length; dataValue = dataValue.WithWrappedValue(m_aviationAlphabet[m_aviationAlphabetIndex]); valueUpdated = true; } break; case >= BuiltInType.Null and <= BuiltInType.Enumeration: break; default: throw ServiceResultException.Unexpected($"Unexpected BuiltInType {builtInType}"); } if (valueUpdated) { // Save new updated value to data store WriteFieldData(variable.Name!, namespaceIndex, dataValue); } } } }