mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 00:56:28 +08:00
352 lines
15 KiB
C#
352 lines
15 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 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.Globalization;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace Opc.Ua.Mcp.Serialization
|
|
{
|
|
/// <summary>
|
|
/// Provides helper methods for converting OPC UA types to/from JSON-friendly representations.
|
|
/// </summary>
|
|
public static class OpcUaJsonHelper
|
|
{
|
|
/// <summary>
|
|
/// Shared JSON serializer options for OPC UA MCP tool results.
|
|
/// </summary>
|
|
public static JsonSerializerOptions JsonOptions { get; } = CreateJsonOptions();
|
|
|
|
/// <summary>
|
|
/// Serializes an object to a JSON string using the OPC UA JSON options.
|
|
/// </summary>
|
|
/// <typeparam name="T"></typeparam>
|
|
public static string Serialize<T>(T value)
|
|
{
|
|
return JsonSerializer.Serialize(value, JsonOptions);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a NodeId from its string representation.
|
|
/// </summary>
|
|
/// <param name="nodeIdString">The NodeId string, e.g. "ns=2;s=MyVariable" or "i=85".</param>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static NodeId ParseNodeId(string nodeIdString)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(nodeIdString))
|
|
{
|
|
throw new ArgumentException("NodeId string cannot be null or empty.", nameof(nodeIdString));
|
|
}
|
|
|
|
return NodeId.Parse(nodeIdString);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses an ExpandedNodeId from its string representation.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static ExpandedNodeId ParseExpandedNodeId(string nodeIdString)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(nodeIdString))
|
|
{
|
|
throw new ArgumentException("ExpandedNodeId string cannot be null or empty.", nameof(nodeIdString));
|
|
}
|
|
|
|
return ExpandedNodeId.Parse(nodeIdString);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a QualifiedName from a string like "2:MyName" or just "MyName".
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static QualifiedName ParseQualifiedName(string qualifiedNameString)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(qualifiedNameString))
|
|
{
|
|
throw new ArgumentException("QualifiedName string cannot be null or empty.", nameof(qualifiedNameString));
|
|
}
|
|
|
|
return QualifiedName.Parse(qualifiedNameString);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a DataValue to a JSON-friendly dictionary.
|
|
/// </summary>
|
|
public static Dictionary<string, object?> DataValueToDict(DataValue dataValue)
|
|
{
|
|
return new Dictionary<string, object?>
|
|
{
|
|
["value"] = VariantToObject(dataValue.WrappedValue),
|
|
["statusCode"] = StatusCodeToString(dataValue.StatusCode),
|
|
["sourceTimestamp"] = dataValue.SourceTimestamp != DateTime.MinValue
|
|
? dataValue.SourceTimestamp.ToString("o", CultureInfo.InvariantCulture) : null,
|
|
["serverTimestamp"] = dataValue.ServerTimestamp != DateTime.MinValue
|
|
? dataValue.ServerTimestamp.ToString("o", CultureInfo.InvariantCulture) : null
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a Variant value to a JSON-friendly object.
|
|
/// </summary>
|
|
public static object? VariantToObject(Variant variant)
|
|
{
|
|
if (variant == Variant.Null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
object? value = variant.AsBoxedObject();
|
|
return value switch
|
|
{
|
|
null => null,
|
|
bool b => b,
|
|
sbyte or byte or short or ushort or int or uint or long or ulong => value,
|
|
float f => f,
|
|
double d => d,
|
|
string s => s,
|
|
DateTime dt => dt.ToString("o", CultureInfo.InvariantCulture),
|
|
Uuid uuid => uuid.ToString(),
|
|
byte[] bytes => Convert.ToBase64String(bytes),
|
|
NodeId nodeId => nodeId.ToString(),
|
|
ExpandedNodeId expNodeId => expNodeId.ToString(),
|
|
QualifiedName qn => qn.ToString(),
|
|
LocalizedText lt => lt.Text,
|
|
StatusCode sc => StatusCodeToString(sc),
|
|
ExtensionObject ext => ExtensionObjectToDict(ext),
|
|
Array array => ArrayToList(array),
|
|
_ => value.ToString()
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a StatusCode to a human-readable string.
|
|
/// </summary>
|
|
public static string StatusCodeToString(StatusCode statusCode)
|
|
{
|
|
return statusCode.SymbolicId ?? string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a <see cref="ReferenceDescription"/> to a JSON-friendly dictionary.
|
|
/// </summary>
|
|
public static Dictionary<string, object?> ReferenceDescriptionToDict(ReferenceDescription reference)
|
|
{
|
|
return new Dictionary<string, object?>
|
|
{
|
|
["nodeId"] = reference.NodeId.ToString(),
|
|
["browseName"] = reference.BrowseName.ToString(),
|
|
["displayName"] = reference.DisplayName.Text,
|
|
["nodeClass"] = reference.NodeClass.ToString(),
|
|
["typeDefinition"] = reference.TypeDefinition.IsNull ? null : reference.TypeDefinition.ToString(),
|
|
["isForward"] = reference.IsForward,
|
|
["referenceTypeId"] = reference.ReferenceTypeId.ToString()
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a <see cref="ResponseHeader"/> to a JSON-friendly dictionary.
|
|
/// </summary>
|
|
public static Dictionary<string, object?> ResponseHeaderToDict(ResponseHeader header)
|
|
{
|
|
return new Dictionary<string, object?>
|
|
{
|
|
["timestamp"] = header.Timestamp.ToString("o", CultureInfo.InvariantCulture),
|
|
["requestHandle"] = header.RequestHandle,
|
|
["serviceResult"] = StatusCodeToString(header.ServiceResult)
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a DiagnosticInfo to a JSON-friendly dictionary.
|
|
/// </summary>
|
|
public static Dictionary<string, object?>? DiagnosticInfoToDict(DiagnosticInfo? diagnosticInfo)
|
|
{
|
|
if (diagnosticInfo == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new Dictionary<string, object?>
|
|
{
|
|
["symbolicId"] = diagnosticInfo.SymbolicId,
|
|
["namespaceUri"] = diagnosticInfo.NamespaceUri,
|
|
["locale"] = diagnosticInfo.Locale,
|
|
["localizedText"] = diagnosticInfo.LocalizedText,
|
|
["additionalInfo"] = diagnosticInfo.AdditionalInfo,
|
|
["innerStatusCode"] = StatusCodeToString(diagnosticInfo.InnerStatusCode)
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a list of StatusCodes to string representations.
|
|
/// </summary>
|
|
public static List<string> StatusCodesToStrings(ArrayOf<StatusCode> results)
|
|
{
|
|
if (results.IsNull)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
return [.. results.ToArray()!.Select(StatusCodeToString)];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a Variant value from a JSON element.
|
|
/// </summary>
|
|
public static Variant JsonElementToVariant(JsonElement element, string? dataType = null)
|
|
{
|
|
return element.ValueKind switch
|
|
{
|
|
JsonValueKind.True => new Variant(true),
|
|
JsonValueKind.False => new Variant(false),
|
|
JsonValueKind.Number when dataType?.Equals("Int32", StringComparison.OrdinalIgnoreCase) == true
|
|
=> new Variant(element.GetInt32()),
|
|
JsonValueKind.Number when dataType?.Equals("UInt32", StringComparison.OrdinalIgnoreCase) == true
|
|
=> new Variant(element.GetUInt32()),
|
|
JsonValueKind.Number when dataType?.Equals("Int16", StringComparison.OrdinalIgnoreCase) == true
|
|
=> new Variant(element.GetInt16()),
|
|
JsonValueKind.Number when dataType?.Equals("UInt16", StringComparison.OrdinalIgnoreCase) == true
|
|
=> new Variant(element.GetUInt16()),
|
|
JsonValueKind.Number when dataType?.Equals("Int64", StringComparison.OrdinalIgnoreCase) == true
|
|
=> new Variant(element.GetInt64()),
|
|
JsonValueKind.Number when dataType?.Equals("UInt64", StringComparison.OrdinalIgnoreCase) == true
|
|
=> new Variant(element.GetUInt64()),
|
|
JsonValueKind.Number when dataType?.Equals("Float", StringComparison.OrdinalIgnoreCase) == true
|
|
=> new Variant(element.GetSingle()),
|
|
JsonValueKind.Number when dataType?.Equals("Double", StringComparison.OrdinalIgnoreCase) == true
|
|
=> new Variant(element.GetDouble()),
|
|
JsonValueKind.Number when dataType?.Equals("Byte", StringComparison.OrdinalIgnoreCase) == true
|
|
=> new Variant(element.GetByte()),
|
|
JsonValueKind.Number when dataType?.Equals("SByte", StringComparison.OrdinalIgnoreCase) == true
|
|
=> new Variant(element.GetSByte()),
|
|
JsonValueKind.Number when element.TryGetInt32(out int i) => new Variant(i),
|
|
JsonValueKind.Number when element.TryGetInt64(out long l) => new Variant(l),
|
|
JsonValueKind.Number => new Variant(element.GetDouble()),
|
|
JsonValueKind.String when dataType?.Equals("DateTime", StringComparison.OrdinalIgnoreCase) == true
|
|
=> new Variant(DateTime.Parse(element.GetString()!, CultureInfo.InvariantCulture)),
|
|
JsonValueKind.String => new Variant(element.GetString()!),
|
|
JsonValueKind.Null or JsonValueKind.Undefined => Variant.Null,
|
|
_ => new Variant(element.GetRawText())
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses an attribute ID from a string or integer.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static uint ParseAttributeId(string? attributeIdStr)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(attributeIdStr))
|
|
{
|
|
return Attributes.Value;
|
|
}
|
|
|
|
if (uint.TryParse(attributeIdStr, CultureInfo.InvariantCulture, out uint numericId))
|
|
{
|
|
return numericId;
|
|
}
|
|
|
|
return attributeIdStr.ToUpperInvariant() switch
|
|
{
|
|
"NODEID" => Attributes.NodeId,
|
|
"NODECLASS" => Attributes.NodeClass,
|
|
"BROWSENAME" => Attributes.BrowseName,
|
|
"DISPLAYNAME" => Attributes.DisplayName,
|
|
"DESCRIPTION" => Attributes.Description,
|
|
"WRITEMASK" => Attributes.WriteMask,
|
|
"USERWRITEMASK" => Attributes.UserWriteMask,
|
|
"ISABSTRACT" => Attributes.IsAbstract,
|
|
"SYMMETRIC" => Attributes.Symmetric,
|
|
"INVERSENAME" => Attributes.InverseName,
|
|
"CONTAINSNOLOOPS" => Attributes.ContainsNoLoops,
|
|
"EVENTNOTIFIER" => Attributes.EventNotifier,
|
|
"VALUE" => Attributes.Value,
|
|
"DATATYPE" => Attributes.DataType,
|
|
"VALUERANK" => Attributes.ValueRank,
|
|
"ARRAYDIMENSIONS" => Attributes.ArrayDimensions,
|
|
"ACCESSLEVEL" => Attributes.AccessLevel,
|
|
"USERACCESSLEVEL" => Attributes.UserAccessLevel,
|
|
"MINIMUMSAMPLINGINTERVAL" => Attributes.MinimumSamplingInterval,
|
|
"HISTORIZING" => Attributes.Historizing,
|
|
"EXECUTABLE" => Attributes.Executable,
|
|
"USEREXECUTABLE" => Attributes.UserExecutable,
|
|
"DATATYPEDEFINITION" => Attributes.DataTypeDefinition,
|
|
"ROLEPERMISSIONS" => Attributes.RolePermissions,
|
|
"USERROLEPERMISSIONS" => Attributes.UserRolePermissions,
|
|
"ACCESSRESTRICTIONS" => Attributes.AccessRestrictions,
|
|
"ACCESSLEVELEX" => Attributes.AccessLevelEx,
|
|
_ => throw new ArgumentException($"Unknown attribute: {attributeIdStr}", nameof(attributeIdStr))
|
|
};
|
|
}
|
|
|
|
private static Dictionary<string, object?> ExtensionObjectToDict(ExtensionObject ext)
|
|
{
|
|
var result = new Dictionary<string, object?>
|
|
{
|
|
["typeId"] = ext.TypeId.ToString()
|
|
};
|
|
|
|
if (ext.TryGetValue(out IEncodeable? encodeable))
|
|
{
|
|
result["body"] = encodeable.ToString();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static List<object?> ArrayToList(Array array)
|
|
{
|
|
var list = new List<object?>(array.Length);
|
|
foreach (object? item in array)
|
|
{
|
|
list.Add(item switch
|
|
{
|
|
Variant v => VariantToObject(v),
|
|
DataValue dv => DataValueToDict(dv),
|
|
_ => item.ToString()
|
|
});
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private static JsonSerializerOptions CreateJsonOptions()
|
|
{
|
|
return new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
};
|
|
}
|
|
}
|
|
}
|