/* ======================================================================== * 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; using Opc.Ua.PubSub.PublishedData; namespace Opc.Ua.PubSub.Encoding { /// /// The JsonDataSetMessage class handler. /// It handles the JsonDataSetMessage encoding /// public class JsonDataSetMessage : UaDataSetMessage { private const string kFieldPayload = "Payload"; private FieldTypeEncodingMask m_fieldTypeEncoding; /// /// Create new instance of with DataSet parameter /// public JsonDataSetMessage(ILogger? logger = null) : this(null, logger) { } /// /// Create new instance of with DataSet parameter /// public JsonDataSetMessage(DataSet? dataSet, ILogger? logger = null) : base(logger!) { DataSet = dataSet!; } /// /// Get JsonDataSetMessageContentMask /// The DataSetWriterMessageContentMask defines the flags for the content of the DataSetMessage header. /// The Json message mapping specific flags are defined by the enum. /// public JsonDataSetMessageContentMask DataSetMessageContentMask { get; set; } /// /// Flag that indicates if the dataset message header is encoded /// public bool HasDataSetMessageHeader { get; set; } /// /// Set DataSetFieldContentMask /// /// The new for this dataset public override void SetFieldContentMask(DataSetFieldContentMask fieldContentMask) { FieldContentMask = fieldContentMask; if (FieldContentMask == DataSetFieldContentMask.None) { // 00 Variant Field Encoding m_fieldTypeEncoding = FieldTypeEncodingMask.Variant; } else if (((int)FieldContentMask & (int)DataSetFieldContentMask.RawData) != 0) { // If the RawData flag is set, all other bits are ignored. // 01 RawData Field Encoding m_fieldTypeEncoding = FieldTypeEncodingMask.RawData; } else if (((int)FieldContentMask & ((int)DataSetFieldContentMask.StatusCode | (int)DataSetFieldContentMask.SourceTimestamp | (int)DataSetFieldContentMask.ServerTimestamp | (int)DataSetFieldContentMask.SourcePicoSeconds | (int)DataSetFieldContentMask.ServerPicoSeconds)) != 0) { // 10 DataValue Field Encoding m_fieldTypeEncoding = FieldTypeEncodingMask.DataValue; } } /// /// Encodes the dataset message /// /// The used to encode this object. /// The field name to be used to encode this object, by default it is null. internal void Encode(PubSubJsonEncoder jsonEncoder, string? fieldName = null) { jsonEncoder.PushStructure(fieldName); if (HasDataSetMessageHeader) { EncodeDataSetMessageHeader(jsonEncoder); } if (DataSet != null) { EncodePayload(jsonEncoder, HasDataSetMessageHeader); } jsonEncoder.PopStructure(); } /// /// Decode dataset from the provided json decoder using the provided . /// /// The json decoder that contains the json stream. /// Number of Messages found in current jsonDecoder. If 0 then there is SingleDataSetMessage /// The name of the Messages list /// The used to decode the data set. internal void DecodePossibleDataSetReader( PubSubJsonDecoder jsonDecoder, int messagesCount, string messagesListName, DataSetReaderDataType dataSetReader) { if (messagesCount == 0) { // check if there shall be a dataset header and decode it if (HasDataSetMessageHeader) { DecodeDataSetMessageHeader(jsonDecoder); // push into PayloadStructure if there was a dataset header jsonDecoder.PushStructure(kFieldPayload); } DecodeErrorReason = ValidateMetadataVersion( dataSetReader?.DataSetMetaData?.ConfigurationVersion!); if (IsMetadataMajorVersionChange) { return; } // handle single dataset with no network message header & no dataset message header (the content of the payload) DataSet = DecodePayloadContent(jsonDecoder, dataSetReader!)!; } else { for (int index = 0; index < messagesCount; index++) { bool wasPush = jsonDecoder.PushArray(messagesListName, index); if (wasPush) { // attempt decoding the DataSet fields DecodePossibleDataSetReader(jsonDecoder, dataSetReader); // redo jsonDecoder stack jsonDecoder.Pop(); if (DataSet != null) { // the dataset was decoded return; } } } } } /// /// Attempt to decode dataset from the KeyValue pairs /// private void DecodePossibleDataSetReader( PubSubJsonDecoder jsonDecoder, DataSetReaderDataType dataSetReader) { // check if there shall be a dataset header and decode it if (HasDataSetMessageHeader) { DecodeDataSetMessageHeader(jsonDecoder); } if (dataSetReader.DataSetWriterId != 0 && DataSetWriterId != dataSetReader.DataSetWriterId) { return; } string? payloadStructureName = kFieldPayload; // try to read "Payload" structure if (!jsonDecoder.ReadField(kFieldPayload, out object token)) { // Decode the Messages element in case there is no "Payload" structure jsonDecoder.ReadField(null, out token); payloadStructureName = null; } if (token is Dictionary payload && dataSetReader.DataSetMetaData != null) { DecodeErrorReason = ValidateMetadataVersion( dataSetReader.DataSetMetaData.ConfigurationVersion); if ((payload.Count > dataSetReader.DataSetMetaData.Fields.Count) || IsMetadataMajorVersionChange) { // filter out payload that has more fields than the searched datasetMetadata or // doesn't pass metadata version return; } // check also the field names from reader, if any extra field names then the payload is not matching foreach (string key in payload.Keys) { FieldMetaData field = dataSetReader.DataSetMetaData.Fields.Find(f => f.Name == key); if (field == null) { // the field from payload was not found in dataSetReader therefore the payload is not suitable to be decoded return; } } } try { // try decoding Payload Structure bool wasPush = jsonDecoder.PushStructure(payloadStructureName); if (wasPush) { DataSet = DecodePayloadContent(jsonDecoder, dataSetReader)!; } } finally { // redo decode stack jsonDecoder.Pop(); } } /// /// Decode the Content of the Payload and create a DataSet object from it /// /// private DataSet? DecodePayloadContent( PubSubJsonDecoder jsonDecoder, DataSetReaderDataType dataSetReader) { DataSetMetaDataType dataSetMetaData = dataSetReader.DataSetMetaData; var dataValues = new List(); for (int index = 0; index < dataSetMetaData?.Fields.Count; index++) { FieldMetaData? fieldMetaData = dataSetMetaData?.Fields[index]; if (jsonDecoder.ReadField(fieldMetaData!.Name, out _)) { switch (m_fieldTypeEncoding) { case FieldTypeEncodingMask.Variant: Variant variantValue = jsonDecoder.ReadVariant(fieldMetaData.Name); dataValues.Add(new DataValue(variantValue)); break; case FieldTypeEncodingMask.RawData: object? value = DecodeRawData( jsonDecoder, dataSetMetaData!.Fields[index], dataSetMetaData.Fields[index].Name!); #pragma warning disable CS0618 // Type or member is obsolete dataValues.Add(new DataValue(new Variant(value!))); #pragma warning restore CS0618 // Type or member is obsolete break; case FieldTypeEncodingMask.DataValue: bool wasPush2 = jsonDecoder.PushStructure(fieldMetaData.Name); var dataValue = new DataValue(Variant.Null); try { if (wasPush2 && jsonDecoder.ReadField("Value", out object token)) { // the Value was encoded using the non reversible json encoding token = DecodeRawData( jsonDecoder, dataSetMetaData!.Fields[index], "Value")!; #pragma warning disable CS0618 // Type or member is obsolete dataValue = new DataValue(new Variant(token!)); #pragma warning restore CS0618 // Type or member is obsolete } else { // handle Good StatusCode that was not encoded if (dataSetMetaData?.Fields[index] .BuiltInType == (byte)BuiltInType.StatusCode) { dataValue = new DataValue(new Variant(StatusCodes.Good)); } } if ((FieldContentMask & DataSetFieldContentMask.StatusCode) != 0 && jsonDecoder.ReadField("StatusCode", out token)) { bool wasPush3 = jsonDecoder.PushStructure("StatusCode"); if (wasPush3) { dataValue = dataValue.WithStatus(jsonDecoder.ReadStatusCode("Code")); jsonDecoder.Pop(); } } if ((FieldContentMask & DataSetFieldContentMask.SourceTimestamp) != 0) { dataValue = dataValue.WithSourceTimestamp( jsonDecoder.ReadDateTime("SourceTimestamp")); } if ((FieldContentMask & DataSetFieldContentMask.SourcePicoSeconds) != 0) { dataValue = dataValue.WithSourcePicoseconds( jsonDecoder.ReadUInt16("SourcePicoseconds")); } if ((FieldContentMask & DataSetFieldContentMask.ServerTimestamp) != 0) { dataValue = dataValue.WithServerTimestamp( jsonDecoder.ReadDateTime("ServerTimestamp")); } if ((FieldContentMask & DataSetFieldContentMask.ServerPicoSeconds) != 0) { dataValue = dataValue.WithServerPicoseconds( jsonDecoder.ReadUInt16("ServerPicoseconds")); } dataValues.Add(dataValue); } finally { if (wasPush2) { jsonDecoder.Pop(); } } break; case FieldTypeEncodingMask.Reserved: break; default: throw ServiceResultException.Unexpected( $"Unexpected FieldDataTypeEncodingMask {m_fieldTypeEncoding}"); } } else { switch (m_fieldTypeEncoding) { case FieldTypeEncodingMask.Variant: case FieldTypeEncodingMask.RawData: // handle StatusCodes.Good which is not encoded and therefore must be created at decode if (dataSetMetaData?.Fields[index] .BuiltInType == (byte)BuiltInType.StatusCode) { dataValues.Add( new DataValue(new Variant(StatusCodes.Good))); } else { // the field is null dataValues.Add(new DataValue(Variant.Null)); } break; case FieldTypeEncodingMask.DataValue: case FieldTypeEncodingMask.Reserved: break; default: throw ServiceResultException.Unexpected( $"Unexpected FieldDataTypeEncodingMask {m_fieldTypeEncoding}"); } } } if (dataValues.Count != dataSetMetaData?.Fields.Count) { return null; } //build the DataSet Fields collection based on the decoded values and the target var dataFields = new List(); for (int i = 0; i < dataValues.Count; i++) { var dataField = new Field { FieldMetaData = dataSetMetaData?.Fields[i], Value = dataValues[i] }; if (ExtensionObject.ToEncodeable(dataSetReader.SubscribedDataSet) is TargetVariablesDataType targetVariablesData && i < targetVariablesData.TargetVariables.Count) { // remember the target Attribute and target nodeId dataField.TargetAttribute = targetVariablesData.TargetVariables[i].AttributeId; dataField.TargetNodeId = targetVariablesData.TargetVariables[i].TargetNodeId; } dataFields.Add(dataField); } // build the dataset object return new DataSet(dataSetMetaData?.Name) { DataSetMetaData = dataSetMetaData, Fields = [.. dataFields], DataSetWriterId = DataSetWriterId, SequenceNumber = SequenceNumber }; } /// /// Encode DataSet message header /// private void EncodeDataSetMessageHeader(PubSubJsonEncoder encoder) { if ((DataSetMessageContentMask & JsonDataSetMessageContentMask.DataSetWriterId) != 0) { encoder.WriteUInt16(nameof(DataSetWriterId), DataSetWriterId); } if ((DataSetMessageContentMask & JsonDataSetMessageContentMask.SequenceNumber) != 0) { encoder.WriteUInt32(nameof(SequenceNumber), SequenceNumber); } if ((DataSetMessageContentMask & JsonDataSetMessageContentMask.MetaDataVersion) != 0) { encoder.WriteEncodeable(nameof(MetaDataVersion), MetaDataVersion); } if ((DataSetMessageContentMask & JsonDataSetMessageContentMask.Timestamp) != 0) { encoder.WriteDateTime(nameof(Timestamp), Timestamp); } if ((DataSetMessageContentMask & JsonDataSetMessageContentMask.Status) != 0) { encoder.WriteStatusCode(nameof(Status), Status); } } /// /// Encodes The DataSet message payload /// internal void EncodePayload(PubSubJsonEncoder jsonEncoder, bool pushStructure = true) { bool forceNamespaceUri = jsonEncoder.ForceNamespaceUri; if (pushStructure) { jsonEncoder.PushStructure(kFieldPayload); } foreach (Field field in DataSet.Fields!) { if (field != null) { EncodeField(jsonEncoder, field); } } if (pushStructure) { jsonEncoder.PopStructure(); } jsonEncoder.ForceNamespaceUri = forceNamespaceUri; } /// /// Encodes a dataSet field /// /// private void EncodeField(PubSubJsonEncoder encoder, Field field) { string fieldName = field.FieldMetaData!.Name!; DataValue fieldValue = field.Value; Variant valueToEncode = fieldValue.WrappedValue; // Only treat an actual StatusCode value equal to Good as null to avoid misencoding if (valueToEncode.TypeInfo.BuiltInType == BuiltInType.StatusCode && valueToEncode.TryGetValue(out StatusCode statusCode) && statusCode == StatusCodes.Good && m_fieldTypeEncoding != FieldTypeEncodingMask.Variant) { valueToEncode = Variant.Null; } if (m_fieldTypeEncoding != FieldTypeEncodingMask.DataValue && StatusCode.IsBad(fieldValue.StatusCode)) { valueToEncode = fieldValue.StatusCode; } switch (m_fieldTypeEncoding) { case FieldTypeEncodingMask.Variant: // If the DataSetFieldContentMask results in a Variant representation, // the field value is encoded as a Variant encoded using the reversible OPC UA JSON Data Encoding // defined in OPC 10000-6. encoder.ForceNamespaceUri = false; encoder.UsingAlternateEncoding( encoder.WriteVariant, fieldName, valueToEncode, PubSubJsonEncoding.Reversible); break; case FieldTypeEncodingMask.RawData: // If the DataSetFieldContentMask results in a RawData representation, // the field value is a Variant encoded using the non-reversible OPC UA JSON Data Encoding // defined in OPC 10000-6 encoder.ForceNamespaceUri = true; encoder.UsingAlternateEncoding( encoder.WriteVariant, fieldName, valueToEncode, PubSubJsonEncoding.NonReversible); break; case FieldTypeEncodingMask.DataValue: var dataValue = new DataValue(valueToEncode); if ((FieldContentMask & DataSetFieldContentMask.StatusCode) != 0) { dataValue = dataValue.WithStatus(fieldValue.StatusCode); } if ((FieldContentMask & DataSetFieldContentMask.SourceTimestamp) != 0) { dataValue = dataValue.WithSourceTimestamp(fieldValue.SourceTimestamp); } if ((FieldContentMask & DataSetFieldContentMask.SourcePicoSeconds) != 0) { dataValue = dataValue.WithSourcePicoseconds(fieldValue.SourcePicoseconds); } if ((FieldContentMask & DataSetFieldContentMask.ServerTimestamp) != 0) { dataValue = dataValue.WithServerTimestamp(fieldValue.ServerTimestamp); } if ((FieldContentMask & DataSetFieldContentMask.ServerPicoSeconds) != 0) { dataValue = dataValue.WithServerPicoseconds(fieldValue.ServerPicoseconds); } // If the DataSetFieldContentMask results in a DataValue representation, // the field value is a DataValue encoded using the non-reversible OPC UA JSON Data Encoding encoder.ForceNamespaceUri = true; encoder.UsingAlternateEncoding( encoder.WriteDataValue, fieldName, dataValue, PubSubJsonEncoding.NonReversible); break; case FieldTypeEncodingMask.Reserved: break; default: throw ServiceResultException.Unexpected( $"Unexpected FieldDataTypeEncodingMask {m_fieldTypeEncoding}"); } } /// /// Decode RawData type /// private object? DecodeRawData( PubSubJsonDecoder jsonDecoder, FieldMetaData fieldMetaData, string fieldName) { if (fieldMetaData.BuiltInType != 0) { try { if (fieldMetaData.ValueRank == ValueRanks.Scalar) { return DecodeRawScalar(jsonDecoder, fieldMetaData.BuiltInType, fieldName); } if (fieldMetaData.ValueRank >= ValueRanks.OneDimension) { return jsonDecoder.ReadArray( fieldName, fieldMetaData.ValueRank, (BuiltInType)fieldMetaData.BuiltInType); } m_logger.LogInformation( "JsonDataSetMessage - Decoding ValueRank = {ValueRank} not supported yet !!!", fieldMetaData.ValueRank); } catch (Exception ex) { m_logger.LogError(ex, "JsonDataSetMessage - Error reading element for RawData."); return StatusCodes.BadDecodingError; } } return null; } /// /// Decodes the DataSetMessageHeader /// private void DecodeDataSetMessageHeader(PubSubJsonDecoder jsonDecoder) { if ((DataSetMessageContentMask & JsonDataSetMessageContentMask.DataSetWriterId) != 0 && jsonDecoder.ReadField(nameof(DataSetWriterId), out _)) { DataSetWriterId = jsonDecoder.ReadUInt16(nameof(DataSetWriterId)); } if ((DataSetMessageContentMask & JsonDataSetMessageContentMask.SequenceNumber) != 0 && jsonDecoder.ReadField(nameof(SequenceNumber), out _)) { SequenceNumber = jsonDecoder.ReadUInt32(nameof(SequenceNumber)); } if ((DataSetMessageContentMask & JsonDataSetMessageContentMask.MetaDataVersion) != 0 && jsonDecoder.ReadField(nameof(MetaDataVersion), out _)) { MetaDataVersion = (jsonDecoder.ReadEncodeable( nameof(MetaDataVersion), typeof(ConfigurationVersionDataType)) as ConfigurationVersionDataType)!; } if ((DataSetMessageContentMask & JsonDataSetMessageContentMask.Timestamp) != 0 && jsonDecoder.ReadField(nameof(Timestamp), out _)) { Timestamp = jsonDecoder.ReadDateTime(nameof(Timestamp)); } if ((DataSetMessageContentMask & JsonDataSetMessageContentMask.Status) != 0 && jsonDecoder.ReadField(nameof(Status), out _)) { Status = jsonDecoder.ReadStatusCode(nameof(Status)); } } /// /// Decode a scalar type /// /// private object? DecodeRawScalar( PubSubJsonDecoder jsonDecoder, byte builtInType, string fieldName) { try { switch ((BuiltInType)builtInType) { case BuiltInType.Boolean: return jsonDecoder.ReadBoolean(fieldName); case BuiltInType.SByte: return jsonDecoder.ReadSByte(fieldName); case BuiltInType.Byte: return jsonDecoder.ReadByte(fieldName); case BuiltInType.Int16: return jsonDecoder.ReadInt16(fieldName); case BuiltInType.UInt16: return jsonDecoder.ReadUInt16(fieldName); case BuiltInType.Int32: return jsonDecoder.ReadInt32(fieldName); case BuiltInType.UInt32: return jsonDecoder.ReadUInt32(fieldName); case BuiltInType.Int64: return jsonDecoder.ReadInt64(fieldName); case BuiltInType.UInt64: return jsonDecoder.ReadUInt64(fieldName); case BuiltInType.Float: return jsonDecoder.ReadFloat(fieldName); case BuiltInType.Double: return jsonDecoder.ReadDouble(fieldName); case BuiltInType.String: return jsonDecoder.ReadString(fieldName); case BuiltInType.DateTime: return jsonDecoder.ReadDateTime(fieldName); case BuiltInType.Guid: return jsonDecoder.ReadGuid(fieldName); case BuiltInType.ByteString: return jsonDecoder.ReadByteString(fieldName); case BuiltInType.XmlElement: return jsonDecoder.ReadXmlElement(fieldName); case BuiltInType.NodeId: return jsonDecoder.ReadNodeId(fieldName); case BuiltInType.ExpandedNodeId: return jsonDecoder.ReadExpandedNodeId(fieldName); case BuiltInType.QualifiedName: return jsonDecoder.ReadQualifiedName(fieldName); case BuiltInType.LocalizedText: return jsonDecoder.ReadLocalizedText(fieldName); case BuiltInType.DataValue: return jsonDecoder.ReadDataValue(fieldName); case BuiltInType.Enumeration: return jsonDecoder.ReadInt32(fieldName); case BuiltInType.Variant: return jsonDecoder.ReadVariant(fieldName); case BuiltInType.ExtensionObject: return jsonDecoder.ReadExtensionObject(fieldName); case BuiltInType.DiagnosticInfo: return jsonDecoder.ReadDiagnosticInfo(fieldName); case BuiltInType.StatusCode: return jsonDecoder.ReadStatusCode(fieldName); case BuiltInType.Null: case BuiltInType.Number: case BuiltInType.Integer: case BuiltInType.UInteger: return null; default: throw ServiceResultException.Unexpected( $"Unexpected BuiltInType {builtInType}"); } } catch (Exception ex) { m_logger.LogError(ex, "JsonDataSetMessage - Error decoding field {Name}", fieldName); return null; } } } }