mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-28 14:09:56 +08:00
## Summary Adds OPC UA WoT Connectivity 1.1 to the stack, together with the Types, source generation, Server and xRegistry work it is built on. This branch is the merge point for the fourteen-PR stack that replaced integration PR #4093; every constituent PR was reviewed and approved separately and is listed below. 414 files changed, +108,936 / -3,199. Roughly 46k added lines of product code, 57k of tests, plus samples, tools and documentation. ## Types - Complete `ExtensionObject` raw-body decoding for binary, XML and JSON bodies by resolving the concrete type through the message context's encodeable factory, and simplify the `Variant` structure helpers by delegating to it (#4128). - Add `IAtomicFileReplace` as an optional `IFileSystem` capability so existing external implementations keep working, with atomic publish for `LocalFileSystem` via `File.Replace`/`File.Move` and for `VirtualFileSystem` by re-keying the in-memory entry (#4128). - Add lossless conversion between WoT documents and NodeSet2 (`Opc.Ua.Wot.WotNodeSetConverter`). A byte-exact `uav:nodeSet` envelope is preserved when requested, the structured `uav:nodes` projection is used when the readable vocabulary is incomplete, and NodeSet2 is otherwise synthesized from readable WoT terms. Unmapped JSON members survive a round trip as pointer-addressed residue (#4131). ## Source generation - Improve the NodeSet to ModelDesign conversion and the generated node state (#4132). - Generate OPC UA models directly from WoT Thing Description files (#4134). ## Server - Add live NodeManager shadow and immediate reload. A reload materializes a new generation beside the active one and switches atomically; the superseded generation is retired gracefully so existing MonitoredItems keep being served until they drain, or immediately when the caller asks for it. Includes the request admission and drain machinery that lets an orderly shutdown wait for admitted requests instead of tearing down underneath them (#4147). - Make subscription transfer between sessions transactional, so a failed transfer leaves neither session holding a partially moved subscription (#4150). ## xRegistry - Allow clients to use explicit registry roots rather than assuming a well-known location (#4130). ## WoT Connectivity - Add the WoT Connectivity 1.1 information model: a registry-first revision layered on the abstract xRegistry base model, incorporating the complete published OPC 10100-1 v1.02 surface as deprecated nodes in the same namespace so existing 1.02 clients keep working (#4139). - Add the protocol binding abstractions and planners that compile WoT forms into executable plans (#4140). - Add the HTTP, Modbus, OPC UA and MQTT binding executors (#4144). - Add the registry and its stores, including document versioning, validation, dependency resolution and refresh (#4146). - Add the registry client (#4142). - Materialize WoT Thing Descriptions into the server address space, deriving types from Thing Models and instances from Thing Descriptions (#4154). - Restore the WoT asset registry bridge so the deprecated 1.02 asset surface is backed by the registry (#4171). - Add the WoT Connectivity samples and complete the documentation (#4156), and restore the remaining coverage and docs (#4172). ## New projects `src/Opc.Ua.WotCon.Bindings`, `src/Opc.Ua.WotCon.Bindings.Mqtt`, `tests/Opc.Ua.WotCon.Bindings.Tests`, `tests/Opc.Ua.WotCon.Samples.Tests`, and the `samples/WotCon` AggregationServer, AggregationClient and FlatTagServer samples. ## Constituent pull requests Merged into this branch top-down, each independently approved: | PR | Title | | --- | --- | | #4128 | [Types] Complete ExtensionObject decoding and add an atomic file replace capability | | #4131 | [Types] Add lossless conversion between WoT documents and NodeSet2 | | #4132 | [SourceGeneration] Improve NodeSet to ModelDesign conversion and generated node state | | #4134 | [SourceGeneration] Generate OPC UA models from WoT Thing Description files | | #4147 | [Server] Add live NodeManager shadow and immediate reload | | #4150 | [Server] Make subscription transfer between sessions transactional | | #4130 | [XRegistry] Allow clients to use explicit registry roots | | #4139 | [WotCon] Add the WoT Connectivity 1.1 information model | | #4140 | [WotCon] Add the WoT protocol binding abstractions and planners | | #4144 | [WotCon] Add the HTTP, Modbus, OPC UA and MQTT binding executors | | #4146 | [WotCon] Add the WoT Connectivity registry and its stores | | #4142 | [WotCon] Add the WoT Connectivity registry client | | #4154 | [WotCon] Materialize WoT Thing Descriptions into the server address space | | #4156 | [WotCon] Add the WoT Connectivity samples and complete the documentation | | #4171 | Restore WoT asset registry bridge | | #4172 | Restore WoT coverage and docs | Supersedes #4093. ## Follow-up The WoT Connectivity and WoT Binding drafts have moved to 1.1-draft2 since this work was authored. Aligning with that revision - the removed group vocabulary, the new projection/View construct, the remaining model vocabulary terms and the two new portable-identity validation rules - is tracked separately and will follow in its own pull request. ## Validation Built for every target framework with zero warnings and zero errors, and validated at the tip of the stack with `Opc.Ua.WotCon.Tests` at 999 passed / 0 failed and `Opc.Ua.Server.Tests` at 4024 passed / 0 failed. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
2850 lines
111 KiB
C#
2850 lines
111 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;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Xml;
|
|
using Opc.Ua.SourceGeneration;
|
|
using Opc.Ua.Types;
|
|
|
|
namespace Opc.Ua.Schema.Model
|
|
{
|
|
/// <summary>
|
|
/// Defines where to add nullable annotations
|
|
/// </summary>
|
|
internal enum NullableAnnotation
|
|
{
|
|
/// <summary>
|
|
/// Types should be non nullable
|
|
/// </summary>
|
|
NonNullable,
|
|
|
|
/// <summary>
|
|
/// Types are nullable except for data types
|
|
/// </summary>
|
|
NullableExceptDataTypes,
|
|
|
|
/// <summary>
|
|
/// All types are nullable
|
|
/// </summary>
|
|
Nullable
|
|
}
|
|
|
|
/// <summary>
|
|
/// Identifies one generated method-argument symbol scope and the names
|
|
/// reserved by that scope's emitted scaffolding.
|
|
/// </summary>
|
|
internal sealed class MethodArgumentCodeNameScope
|
|
{
|
|
public MethodArgumentCodeNameScope(params string[] reservedNames)
|
|
: this(null, reservedNames)
|
|
{
|
|
}
|
|
|
|
public MethodArgumentCodeNameScope(
|
|
Func<string, bool, IEnumerable<string>> getCollisionIdentifiers,
|
|
params string[] reservedNames)
|
|
{
|
|
ReservedNames = reservedNames == null ? [] : [.. reservedNames];
|
|
m_getCollisionIdentifiers = getCollisionIdentifiers;
|
|
}
|
|
|
|
public IReadOnlyCollection<string> ReservedNames { get; }
|
|
|
|
public IEnumerable<string> GetCollisionIdentifiers(
|
|
string identifier,
|
|
bool output)
|
|
{
|
|
return m_getCollisionIdentifiers?.Invoke(identifier, output) ?? [identifier];
|
|
}
|
|
|
|
private readonly Func<string, bool, IEnumerable<string>> m_getCollisionIdentifiers;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dotnet code generation support.
|
|
/// </summary>
|
|
internal static class ModelDesignExtensions
|
|
{
|
|
/// <summary>
|
|
/// Get basic data type from data type design.
|
|
/// </summary>
|
|
/// <param name="dataType"></param>
|
|
/// <returns></returns>
|
|
public static BasicDataType DetermineBasicDataType(this DataTypeDesign dataType)
|
|
{
|
|
if (dataType == null)
|
|
{
|
|
return BasicDataType.BaseDataType;
|
|
}
|
|
if (dataType.IsOptionSet)
|
|
|
|
{
|
|
return BasicDataType.Enumeration;
|
|
}
|
|
// check if it is a built in data type.
|
|
if (dataType.IsBasicDataType(out BasicDataType basicDataType))
|
|
{
|
|
return basicDataType;
|
|
}
|
|
|
|
// recursively search hierarchy if not base data type yet
|
|
BasicDataType basicType = DetermineBasicDataType(
|
|
dataType.BaseTypeNode as DataTypeDesign);
|
|
|
|
// data type is user defined if a sub-type of structure.
|
|
if (basicType == BasicDataType.Structure)
|
|
{
|
|
return BasicDataType.UserDefined;
|
|
}
|
|
return basicType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get type node state class name
|
|
/// </summary>
|
|
public static string GetNodeStateClassName(
|
|
this TypeDesign type,
|
|
Namespace[] namespaces)
|
|
{
|
|
string className = type.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
return CoreUtils.Format("{0}State", className);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the class name to use when creating an instance of the type.
|
|
/// </summary>
|
|
public static string GetNodeStateClassName(
|
|
this InstanceDesign instance,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
bool asFactory = false,
|
|
bool applyStandardFallback = true)
|
|
{
|
|
if (instance is MethodDesign method)
|
|
{
|
|
string className;
|
|
XmlQualifiedName methodStateIdentity =
|
|
MethodDesignArgumentResolver.ResolveMethodStateIdentity(method);
|
|
if (methodStateIdentity != null)
|
|
{
|
|
className =
|
|
methodStateIdentity.AsFullyQualifiedTypeSymbol(namespaces);
|
|
|
|
if (className.EndsWith("MethodType", StringComparison.Ordinal))
|
|
{
|
|
className = className[..^"MethodType".Length];
|
|
}
|
|
else if (className.EndsWith("Type", StringComparison.Ordinal))
|
|
{
|
|
className = className[..^"Type".Length];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
className = method.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
}
|
|
|
|
if (MethodDesignArgumentResolver.HasMethodArguments(method))
|
|
{
|
|
string typedClassName = CoreUtils.Format(
|
|
"{0}{1}MethodState",
|
|
asFactory ? "new " : string.Empty,
|
|
className);
|
|
// Degrade a reference to a standard global::Opc.Ua.*MethodState
|
|
// to the base MethodState when a curated Opc.Ua.Core omits the
|
|
// typed class and the current model pass does not declare it
|
|
// (e.g. the FileDirectoryType methods on a Robotics Programs
|
|
// object). Model-generation-scoped; see
|
|
// StandardMethodStateFallback.
|
|
if (applyStandardFallback &&
|
|
StandardMethodStateFallback.ShouldFallBackToBase(typedClassName))
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}global::Opc.Ua.MethodState",
|
|
asFactory ? "new " : string.Empty);
|
|
}
|
|
return typedClassName;
|
|
}
|
|
|
|
return CoreUtils.Format(
|
|
"{0}global::Opc.Ua.MethodState",
|
|
asFactory ? "new " : string.Empty);
|
|
}
|
|
|
|
if (instance is not VariableDesign variable)
|
|
|
|
{
|
|
return GetNodeStateNameSimple(instance.TypeDefinitionNode);
|
|
}
|
|
var variableType = instance.TypeDefinitionNode as VariableTypeDesign;
|
|
|
|
// check if the variable type restricted the datatype to eliminate the
|
|
// need for a template parameter.
|
|
if (variableType.DataTypeNode.IsTemplateParameterRequired(variableType.ValueRank))
|
|
{
|
|
return GetNodeStateNameSimple(variableType);
|
|
}
|
|
|
|
// check if the variable instance did not restrict the datatype.
|
|
if (!variable.DataTypeNode.IsTemplateParameterRequired(variable.ValueRank))
|
|
{
|
|
return GetNodeStateNameSimple(variableType);
|
|
}
|
|
|
|
// instance restricted the datatype but the type did not.
|
|
string scalarName = GetDotNetTypeName(
|
|
variable.DataTypeNode,
|
|
targetNamespace,
|
|
namespaces,
|
|
nullable: NullableAnnotation.NonNullable,
|
|
true);
|
|
|
|
if (variable.ValueRank == ValueRank.Array)
|
|
{
|
|
if (asFactory)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}State<global::Opc.Ua.ArrayOf<{1}>>.With<{2}>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName,
|
|
GetVariantBuilder(variable.DataTypeNode, scalarName));
|
|
}
|
|
return CoreUtils.Format(
|
|
"{0}State<global::Opc.Ua.ArrayOf<{1}>>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName);
|
|
}
|
|
|
|
if (variable.ValueRank == ValueRank.OneOrMoreDimensions &&
|
|
variable.DataTypeNode.SupportsMatrixOf() &&
|
|
variable.DataTypeNode.BasicDataType
|
|
is not BasicDataType.BaseDataType
|
|
and not BasicDataType.Number
|
|
and not BasicDataType.UInteger
|
|
and not BasicDataType.Integer)
|
|
{
|
|
if (asFactory)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}State<global::Opc.Ua.MatrixOf<{1}>>.With<{2}>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName,
|
|
GetVariantBuilder(variable.DataTypeNode, scalarName));
|
|
}
|
|
return CoreUtils.Format(
|
|
"{0}State<global::Opc.Ua.MatrixOf<{1}>>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName);
|
|
}
|
|
|
|
if (IsIndeterminateType(variable))
|
|
{
|
|
return GetNodeStateNameSimple(variableType);
|
|
}
|
|
|
|
// add hack for TwoStateDiscreteType which always has to be bool.
|
|
if (variableType.SymbolicName ==
|
|
new XmlQualifiedName("TwoStateDiscreteType", Namespaces.OpcUa))
|
|
{
|
|
return GetNodeStateNameSimple(variableType);
|
|
}
|
|
|
|
if (asFactory)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}State<{1}>.With<{2}>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName,
|
|
GetVariantBuilder(variable.DataTypeNode, scalarName));
|
|
}
|
|
|
|
return CoreUtils.Format(
|
|
"{0}State<{1}>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName);
|
|
|
|
string GetNodeStateNameSimple(TypeDesign variableType)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}{1}State",
|
|
asFactory ? "new " : string.Empty,
|
|
GetClassName(variableType, namespaces));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the variant builder to use to access or mutate a variant of type T
|
|
/// </summary>
|
|
public static string GetVariantBuilder(
|
|
this DataTypeDesign datatype,
|
|
string typeName)
|
|
{
|
|
switch (datatype.BasicDataType)
|
|
{
|
|
case BasicDataType.UserDefined:
|
|
if (datatype.IsEnumeration)
|
|
{
|
|
return CoreUtils.Format("global::Opc.Ua.EnumerationBuilder<{0}>", typeName);
|
|
}
|
|
return CoreUtils.Format("global::Opc.Ua.StructureBuilder<{0}>", typeName);
|
|
case BasicDataType.Enumeration:
|
|
if (datatype.SymbolicId ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
return "global::Opc.Ua.VariantBuilder";
|
|
}
|
|
if (datatype.IsOptionSet)
|
|
{
|
|
return GetVariantBuilder((DataTypeDesign)datatype.BaseTypeNode, typeName);
|
|
}
|
|
return CoreUtils.Format("global::Opc.Ua.EnumerationBuilder<{0}>", typeName);
|
|
default:
|
|
return "global::Opc.Ua.VariantBuilder";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the class name to use when creating an instance of the type.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static string GetBaseClassName(this TypeDesign type, Namespace[] namespaces)
|
|
{
|
|
if (type?.BaseTypeNode?.SymbolicName == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(type));
|
|
}
|
|
if (type is not DataTypeDesign dataType ||
|
|
dataType.BaseTypeNode is not DataTypeDesign dtd)
|
|
{
|
|
return type.BaseTypeNode.GetClassName(namespaces);
|
|
}
|
|
|
|
if (dtd.BasicDataType == BasicDataType.Structure)
|
|
|
|
{
|
|
return "global::Opc.Ua.IEncodeable";
|
|
}
|
|
return dtd.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fully qualify a symbol
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string AsFullyQualifiedTypeSymbol(
|
|
this XmlQualifiedName symbol,
|
|
Namespace[] namespaces,
|
|
string alternativeName = null)
|
|
{
|
|
string name = alternativeName ?? symbol?.Name;
|
|
if (string.IsNullOrEmpty(name))
|
|
{
|
|
throw new ArgumentException("Symbol name is empty");
|
|
}
|
|
string ns = namespaces.GetNamespacePrefix(symbol?.Namespace);
|
|
if (string.IsNullOrEmpty(ns))
|
|
{
|
|
return name;
|
|
}
|
|
return CoreUtils.Format("global::{0}.{1}", ns, name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the class name to use when creating an instance of the type.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool IsDerivedDataType(this TypeDesign type)
|
|
{
|
|
if (type is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(type));
|
|
}
|
|
if (type is not DataTypeDesign ||
|
|
type.BaseTypeNode is not DataTypeDesign dtd)
|
|
{
|
|
return true;
|
|
}
|
|
return dtd.BasicDataType != BasicDataType.Structure;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a qualifier for the namespace to use in code.
|
|
/// </summary>
|
|
public static string GetNamespacePrefix(this Namespace[] namespaces, string namespaceUri)
|
|
{
|
|
if (namespaces != null)
|
|
{
|
|
foreach (Namespace ns in namespaces)
|
|
{
|
|
if (ns.Value == namespaceUri)
|
|
{
|
|
return CoreUtils.Format("{0}", ns.Prefix);
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a constant string for the namespace uri.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string GetConstantSymbolForNamespace(
|
|
this Namespace[] namespaces,
|
|
string namespaceUri)
|
|
{
|
|
if (namespaces is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(namespaces));
|
|
}
|
|
Namespace ns = namespaces.GetNamespace(namespaceUri);
|
|
if (ns == null)
|
|
{
|
|
return null;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(ns.Name))
|
|
{
|
|
throw new ArgumentException("Found namespace has no name");
|
|
}
|
|
if (string.IsNullOrWhiteSpace(ns.Prefix)) // TODO: should this work?
|
|
{
|
|
return CoreUtils.Format("Namespaces.{0}", ns.Name);
|
|
}
|
|
return CoreUtils.Format("global::{0}.Namespaces.{1}", ns.Prefix, ns.Name);
|
|
}
|
|
|
|
public static bool IsOverriddenWithSameClass(
|
|
this InstanceDesign instance,
|
|
string targetNamespace,
|
|
Namespace[] namespaces)
|
|
{
|
|
if (instance is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(instance));
|
|
}
|
|
if (!instance.IsOverridden())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string x = instance.GetMergedInstance().GetNodeStateClassName(targetNamespace, namespaces);
|
|
string y = instance.OveriddenNode.GetNodeStateClassName(targetNamespace, namespaces);
|
|
|
|
if (y.StartsWith("BaseDataVariableState<", StringComparison.Ordinal) &&
|
|
x.StartsWith("BaseDataVariableState<", StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return x == y;
|
|
}
|
|
|
|
private static bool IsIndeterminateType(this InstanceDesign instance)
|
|
{
|
|
if (instance is VariableDesign variable)
|
|
{
|
|
if (variable.ValueRank is not ValueRank.Scalar and not ValueRank.Array)
|
|
{
|
|
// Matrix-rank with a concrete data type that supports
|
|
// `MatrixOf<T>` is determinate - the generator emits a
|
|
// typed `State<MatrixOf<T>>` for it. Everything else
|
|
// (ScalarOrArray, ScalarOrOneDimension, Any, or matrix
|
|
// over an abstract numeric base) stays on the Variant
|
|
// fallback.
|
|
return variable.ValueRank != ValueRank.OneOrMoreDimensions ||
|
|
variable.DataTypeNode == null ||
|
|
!variable.DataTypeNode.SupportsMatrixOf() ||
|
|
!(variable.DataTypeNode.BasicDataType
|
|
is not BasicDataType.BaseDataType
|
|
and not BasicDataType.Number
|
|
and not BasicDataType.UInteger
|
|
and not BasicDataType.Integer);
|
|
}
|
|
if (variable.DataType ==
|
|
new XmlQualifiedName(BrowseNames.Enumeration, Namespaces.OpcUa))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static string EnsureUniqueEnumName(this Parameter target)
|
|
{
|
|
if (target?.Parent is DataTypeDesign dt && dt.HasFields && dt.Fields != null)
|
|
{
|
|
HashSet<string> names = [];
|
|
|
|
foreach (Parameter field in dt.Fields)
|
|
{
|
|
int count = 1;
|
|
string check = field.Name;
|
|
|
|
if (names.Contains(check))
|
|
{
|
|
check = field.Name + "_" + field.Identifier;
|
|
}
|
|
|
|
while (names.Contains(check))
|
|
{
|
|
check = field.Name + "_v" + count++;
|
|
}
|
|
|
|
if (target.Identifier == field.Identifier)
|
|
|
|
{
|
|
return check;
|
|
}
|
|
names.Add(check);
|
|
}
|
|
}
|
|
|
|
return target?.Name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the canonical C# identifier for a method argument.
|
|
/// </summary>
|
|
internal static string GetGeneratedCodeIdentifier(
|
|
this Parameter field,
|
|
bool upperCamelCase = false,
|
|
MethodArgumentCodeNameScope scope = null)
|
|
{
|
|
string name = field?.Name;
|
|
scope ??= s_defaultMethodArgumentCodeNameScope;
|
|
if (field != null)
|
|
{
|
|
lock (s_generatedCodeNamesLock)
|
|
{
|
|
if (s_generatedCodeNames.TryGetValue(field, out GeneratedCodeNameState state) &&
|
|
state.TryGetName(scope, out string generatedName) &&
|
|
!string.IsNullOrEmpty(generatedName))
|
|
{
|
|
name = generatedName;
|
|
}
|
|
}
|
|
}
|
|
return name.ToCSharpIdentifier(upperCamelCase);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns unique generated-code names against the requested scope while
|
|
/// preserving the authored OPC UA names.
|
|
/// </summary>
|
|
internal static void AssignMethodArgumentCodeNames(
|
|
this MethodDesign method,
|
|
MethodArgumentCodeNameScope scope = null,
|
|
params string[] additionalReservedNames)
|
|
{
|
|
AssignMethodArgumentCodeNames(
|
|
method?.InputArguments,
|
|
method?.OutputArguments,
|
|
scope,
|
|
additionalReservedNames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns unique generated-code names for an effective method signature.
|
|
/// </summary>
|
|
internal static void AssignMethodArgumentCodeNames(
|
|
Parameter[] inputArguments,
|
|
Parameter[] outputArguments,
|
|
MethodArgumentCodeNameScope scope = null,
|
|
params string[] additionalReservedNames)
|
|
{
|
|
scope ??= s_defaultMethodArgumentCodeNameScope;
|
|
lock (s_generatedCodeNamesLock)
|
|
{
|
|
ClearGeneratedCodeNames(inputArguments, scope);
|
|
ClearGeneratedCodeNames(outputArguments, scope);
|
|
|
|
var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
AddReservedMethodArgumentNames(usedNames, scope.ReservedNames);
|
|
AddReservedMethodArgumentNames(usedNames, additionalReservedNames);
|
|
AssignUniqueGeneratedCodeNames(
|
|
inputArguments,
|
|
usedNames,
|
|
output: false,
|
|
scope);
|
|
AssignUniqueGeneratedCodeNames(
|
|
outputArguments,
|
|
usedNames,
|
|
output: true,
|
|
scope);
|
|
}
|
|
}
|
|
|
|
private static void AddReservedMethodArgumentNames(
|
|
HashSet<string> usedNames,
|
|
IEnumerable<string> reservedNames)
|
|
{
|
|
if (reservedNames == null)
|
|
{
|
|
return;
|
|
}
|
|
foreach (string reservedName in reservedNames)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(reservedName))
|
|
{
|
|
usedNames.Add(GetIdentifierCollisionKey(
|
|
reservedName.ToCSharpIdentifier()));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AssignUniqueGeneratedCodeNames(
|
|
Parameter[] arguments,
|
|
HashSet<string> usedNames,
|
|
bool output,
|
|
MethodArgumentCodeNameScope scope)
|
|
{
|
|
if (arguments == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (Parameter argument in arguments)
|
|
{
|
|
if (argument == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string identifier = argument.Name.ToCSharpIdentifier();
|
|
if (TryReserveGeneratedIdentifier(
|
|
identifier,
|
|
output,
|
|
scope,
|
|
usedNames))
|
|
{
|
|
SetGeneratedCodeName(argument, identifier, scope);
|
|
continue;
|
|
}
|
|
|
|
string stem = identifier.TrimStart('@');
|
|
int suffix = output ? 1 : 2;
|
|
while (true)
|
|
{
|
|
string suffixText;
|
|
if (output)
|
|
{
|
|
suffixText = suffix == 1 ?
|
|
"Out" :
|
|
"Out" + suffix.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
else
|
|
{
|
|
suffixText = suffix.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
string candidate = (stem + suffixText).ToCSharpIdentifier();
|
|
suffix++;
|
|
if (TryReserveGeneratedIdentifier(
|
|
candidate,
|
|
output,
|
|
scope,
|
|
usedNames))
|
|
{
|
|
SetGeneratedCodeName(argument, candidate, scope);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool TryReserveGeneratedIdentifier(
|
|
string identifier,
|
|
bool output,
|
|
MethodArgumentCodeNameScope scope,
|
|
HashSet<string> usedNames)
|
|
{
|
|
string[] collisionKeys =
|
|
[
|
|
.. scope.GetCollisionIdentifiers(identifier, output)
|
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
|
.Select(GetIdentifierCollisionKey)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
];
|
|
if (collisionKeys.Any(usedNames.Contains))
|
|
{
|
|
return false;
|
|
}
|
|
foreach (string collisionKey in collisionKeys)
|
|
{
|
|
usedNames.Add(collisionKey);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static void ClearGeneratedCodeNames(
|
|
Parameter[] arguments,
|
|
MethodArgumentCodeNameScope scope)
|
|
{
|
|
if (arguments == null)
|
|
{
|
|
return;
|
|
}
|
|
foreach (Parameter argument in arguments)
|
|
{
|
|
if (argument != null &&
|
|
s_generatedCodeNames.TryGetValue(
|
|
argument,
|
|
out GeneratedCodeNameState state))
|
|
{
|
|
state.ClearName(scope);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string GetIdentifierCollisionKey(string identifier)
|
|
{
|
|
return identifier.TrimStart('@');
|
|
}
|
|
|
|
private static void SetGeneratedCodeName(
|
|
Parameter argument,
|
|
string name,
|
|
MethodArgumentCodeNameScope scope)
|
|
{
|
|
s_generatedCodeNames.GetOrCreateValue(argument).SetName(scope, name);
|
|
}
|
|
|
|
private sealed class GeneratedCodeNameState
|
|
{
|
|
public bool TryGetName(
|
|
MethodArgumentCodeNameScope scope,
|
|
out string name)
|
|
{
|
|
return m_names.TryGetValue(scope, out name);
|
|
}
|
|
|
|
public void SetName(
|
|
MethodArgumentCodeNameScope scope,
|
|
string name)
|
|
{
|
|
m_names[scope] = name;
|
|
}
|
|
|
|
public void ClearName(MethodArgumentCodeNameScope scope)
|
|
{
|
|
m_names.Remove(scope);
|
|
}
|
|
|
|
private readonly Dictionary<MethodArgumentCodeNameScope, string> m_names = [];
|
|
}
|
|
|
|
private static readonly MethodArgumentCodeNameScope
|
|
s_defaultMethodArgumentCodeNameScope = new();
|
|
private static readonly System.Threading.Lock s_generatedCodeNamesLock = new();
|
|
private static readonly ConditionalWeakTable<Parameter, GeneratedCodeNameState>
|
|
s_generatedCodeNames = new();
|
|
|
|
/// <summary>
|
|
/// Returns the field name of a child node.
|
|
/// </summary>
|
|
public static string GetChildFieldName(this Parameter field)
|
|
{
|
|
if (string.IsNullOrEmpty(field?.Name))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
return field.Name.ToSafeSymbolName(true, "m_");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the field name of a child node.
|
|
/// </summary>
|
|
public static string GetChildFieldName(this InstanceDesign instance)
|
|
{
|
|
if (string.IsNullOrEmpty(instance?.SymbolicName?.Name))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
string name = instance.SymbolicName.Name.ToSafeSymbolName(true, "m_");
|
|
|
|
if (instance is MethodDesign)
|
|
{
|
|
return CoreUtils.Format("{0}Method", name);
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fixes class names for nodes.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string GetClassName(this TypeDesign node, Namespace[] namespaces)
|
|
{
|
|
if (node is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(node));
|
|
}
|
|
if (node is DataTypeDesign)
|
|
|
|
{
|
|
return node.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
}
|
|
if (node is ObjectTypeDesign objectType &&
|
|
objectType.ClassName == "ObjectSource")
|
|
{
|
|
return "global::Opc.Ua.BaseObject";
|
|
}
|
|
|
|
if (node is VariableTypeDesign variableType &&
|
|
variableType.ClassName is "DataVariable" or "BaseVariable")
|
|
{
|
|
return "global::Opc.Ua.BaseDataVariable";
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(node.ClassName))
|
|
{
|
|
return node.SymbolicName.AsFullyQualifiedTypeSymbol(
|
|
namespaces, node.ClassName);
|
|
}
|
|
return node.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the NodeClass of a Node
|
|
/// </summary>
|
|
public static string GetNodeClassAsString(this NodeDesign node)
|
|
{
|
|
if (node is VariableDesign)
|
|
{
|
|
return "Variable";
|
|
}
|
|
if (node is VariableTypeDesign)
|
|
|
|
{
|
|
return "VariableType";
|
|
}
|
|
if (node is ObjectDesign)
|
|
|
|
{
|
|
return "Object";
|
|
}
|
|
if (node is ObjectTypeDesign)
|
|
|
|
{
|
|
return "ObjectType";
|
|
}
|
|
if (node is ReferenceTypeDesign)
|
|
|
|
{
|
|
return "ReferenceType";
|
|
}
|
|
if (node is DataTypeDesign)
|
|
|
|
{
|
|
return "DataType";
|
|
}
|
|
if (node is MethodDesign)
|
|
|
|
{
|
|
return "Method";
|
|
}
|
|
if (node is ViewDesign)
|
|
|
|
{
|
|
return "View";
|
|
}
|
|
return "Node";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the event notifier flag onto a string.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static string GetEventNotifierAsCode(this ObjectTypeDesign objectType)
|
|
{
|
|
if (objectType is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(objectType));
|
|
}
|
|
if (objectType.SupportsEvents)
|
|
{
|
|
return "global::Opc.Ua.EventNotifiers.SubscribeToEvents";
|
|
}
|
|
return "global::Opc.Ua.EventNotifiers.None";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the value rank enumeration onto a string.
|
|
/// </summary>
|
|
public static string GetValueRankAsCode(this ValueRank valueRank, string arrayDimensions)
|
|
{
|
|
switch (valueRank)
|
|
{
|
|
case ValueRank.Array:
|
|
return "global::Opc.Ua.ValueRanks.OneDimension";
|
|
case ValueRank.Scalar:
|
|
return "global::Opc.Ua.ValueRanks.Scalar";
|
|
case ValueRank.Any:
|
|
case ValueRank.ScalarOrArray:
|
|
return "global::Opc.Ua.ValueRanks.Any";
|
|
case ValueRank.ScalarOrOneDimension:
|
|
return "global::Opc.Ua.ValueRanks.ScalarOrOneDimension";
|
|
case ValueRank.OneOrMoreDimensions:
|
|
if (string.IsNullOrWhiteSpace(arrayDimensions))
|
|
{
|
|
return "global::Opc.Ua.ValueRanks.OneOrMoreDimensions";
|
|
}
|
|
|
|
// TODO: "is ,,, not considered 3 dim?
|
|
|
|
string[] dimensions = arrayDimensions.Split([','], StringSplitOptions.RemoveEmptyEntries);
|
|
int dims = dimensions.Length + 1;
|
|
if (dims == 1)
|
|
{
|
|
return "global::Opc.Ua.ValueRanks.OneDimension";
|
|
}
|
|
if (dims == 2)
|
|
{
|
|
return "global::Opc.Ua.ValueRanks.TwoDimensions";
|
|
}
|
|
return CoreUtils.Format("{0}", dims);
|
|
}
|
|
|
|
return "global::Opc.Ua.ValueRanks.Any";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the MinimumSamplingInterval onto a constant.
|
|
/// </summary>
|
|
public static string GetMinimumSamplingIntervalAsCode(this VariableTypeDesign variableType)
|
|
{
|
|
return variableType.MinimumSamplingInterval switch
|
|
{
|
|
-1 => "global::Opc.Ua.MinimumSamplingIntervals.Indeterminate",
|
|
0 => "global::Opc.Ua.MinimumSamplingIntervals.Continuous",
|
|
_ => variableType.MinimumSamplingInterval.ToString(CultureInfo.InvariantCulture)
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether this is a reference type and implicitly nullable
|
|
/// </summary>
|
|
public static bool IsDotNetReferenceType(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank)
|
|
{
|
|
return !IsDotNetValueType(dataType, valueRank);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the data type supports equality comparison in .NET.
|
|
/// </summary>
|
|
public static bool IsDotNetEqualityComparable(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank)
|
|
{
|
|
if (IsDotNetValueType(dataType, valueRank))
|
|
{
|
|
return true;
|
|
}
|
|
if (valueRank == ValueRank.Scalar)
|
|
{
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.String:
|
|
// Use equality for strings
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the data type is a value type in .NET.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool IsDotNetValueType(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank)
|
|
{
|
|
if (dataType is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(dataType));
|
|
}
|
|
if (valueRank != ValueRank.Scalar)
|
|
{
|
|
return true;
|
|
// ArrayOf/MatrixOf/Variant;
|
|
}
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
case BasicDataType.SByte:
|
|
case BasicDataType.Byte:
|
|
case BasicDataType.Int16:
|
|
case BasicDataType.UInt16:
|
|
case BasicDataType.Int32:
|
|
case BasicDataType.UInt32:
|
|
case BasicDataType.Int64:
|
|
case BasicDataType.UInt64:
|
|
case BasicDataType.Float:
|
|
case BasicDataType.Double:
|
|
case BasicDataType.DateTime:
|
|
case BasicDataType.Guid:
|
|
case BasicDataType.NodeId:
|
|
case BasicDataType.ExpandedNodeId:
|
|
case BasicDataType.QualifiedName:
|
|
case BasicDataType.LocalizedText:
|
|
case BasicDataType.StatusCode:
|
|
case BasicDataType.Structure: // Extension object
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType: // Variant
|
|
case BasicDataType.Enumeration when !dataType.IsOptionSet:
|
|
case BasicDataType.UserDefined when dataType.IsEnumeration:
|
|
case BasicDataType.ByteString:
|
|
case BasicDataType.XmlElement:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the type needs to be cloned
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool NeedsCloning(this DataTypeDesign dataType)
|
|
{
|
|
if (dataType is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(dataType));
|
|
}
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.DataValue:
|
|
case BasicDataType.Structure: // Extension object
|
|
case BasicDataType.BaseDataType: // Variant
|
|
case BasicDataType.UserDefined when !dataType.IsEnumeration:
|
|
case BasicDataType.Enumeration when dataType.IsOptionSet:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the data type can be expressed as a typed
|
|
/// <see cref="MatrixOf{T}"/> in generated code, i.e. whether
|
|
/// <c>Variant.From(MatrixOf<T>)</c> /
|
|
/// <c>Variant.GetXxxMatrix()</c> round-trip APIs exist.
|
|
/// All BasicDataType values except <see cref="BasicDataType.DiagnosticInfo"/>
|
|
/// (which is not a valid structure field per OPC UA Part 5) are
|
|
/// supported.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool SupportsMatrixOf(this DataTypeDesign dataType)
|
|
{
|
|
if (dataType is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(dataType));
|
|
}
|
|
return dataType.BasicDataType != BasicDataType.DiagnosticInfo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether a template parameter is required.
|
|
/// </summary>
|
|
public static string GetValueAsCode(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank,
|
|
System.Xml.XmlElement defaultValue,
|
|
object decodedValue,
|
|
bool valueAsVariant,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
IServiceMessageContext context,
|
|
Func<string> onUnknownElement = null)
|
|
{
|
|
TypeInfo decodedValueType = default;
|
|
string defaultString = valueAsVariant ?
|
|
"global::Opc.Ua.Variant.Null" :
|
|
"default";
|
|
|
|
if (decodedValue == null && defaultValue != null)
|
|
{
|
|
// <opc:DefaultValue>
|
|
// <uax:Boolean>true</uax:Boolean>
|
|
// </opc:DefaultValue >
|
|
|
|
using var decoder = new XmlDecoder((XmlElement)defaultValue, context);
|
|
Variant variant = decoder.ReadVariantValue(null, default);
|
|
decodedValueType = variant.TypeInfo;
|
|
decodedValue = variant.AsBoxedObject(Variant.BoxingBehavior.Legacy);
|
|
}
|
|
|
|
if (valueRank == ValueRank.Array)
|
|
{
|
|
return dataType.GetArrayValueAsCode(
|
|
defaultValue,
|
|
decodedValue,
|
|
valueAsVariant,
|
|
targetNamespace,
|
|
namespaces,
|
|
onUnknownElement)
|
|
?? defaultString;
|
|
}
|
|
|
|
if (valueRank == ValueRank.Scalar &&
|
|
dataType.BasicDataType != BasicDataType.BaseDataType)
|
|
{
|
|
return dataType.GetScalarValueAsCode(
|
|
defaultValue,
|
|
decodedValue,
|
|
decodedValueType,
|
|
valueAsVariant,
|
|
targetNamespace,
|
|
namespaces,
|
|
onUnknownElement)
|
|
?? defaultString;
|
|
}
|
|
|
|
return onUnknownElement?.Invoke() ?? defaultString;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get default for a scalar type
|
|
/// </summary>
|
|
internal static string GetScalarValueAsCode(
|
|
this DataTypeDesign dataType,
|
|
System.Xml.XmlElement defaultValue,
|
|
object decodedValue,
|
|
TypeInfo decodedValueType,
|
|
bool valueAsVariant,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
Func<string> onUnknownElement = null)
|
|
{
|
|
// Note that we return a typed value since we initialize a object/variant
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
if (decodedValue is not bool boolValue)
|
|
{
|
|
boolValue = false;
|
|
}
|
|
return MakeReturnType(boolValue ? "true" : "false");
|
|
case BasicDataType.SByte:
|
|
if (decodedValue is not sbyte sbyteValue)
|
|
{
|
|
sbyteValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(sbyte){0}", sbyteValue));
|
|
case BasicDataType.Byte:
|
|
if (decodedValue is not byte byteValue)
|
|
{
|
|
byteValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(byte){0}", byteValue));
|
|
case BasicDataType.Int16:
|
|
if (decodedValue is not short shortValue)
|
|
{
|
|
shortValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(short){0}", shortValue));
|
|
case BasicDataType.UInt16:
|
|
if (decodedValue is not ushort ushortValue)
|
|
{
|
|
ushortValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(ushort){0}", ushortValue));
|
|
case BasicDataType.Int32:
|
|
if (decodedValue is not int intValue)
|
|
{
|
|
intValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(int){0}", intValue));
|
|
case BasicDataType.UInt32:
|
|
if (decodedValue is not uint uintValue)
|
|
{
|
|
uintValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(uint){0}", uintValue));
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.Int64:
|
|
if (decodedValue is not long longValue)
|
|
{
|
|
longValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(long){0}", longValue));
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.UInt64:
|
|
if (decodedValue is not ulong ulongValue)
|
|
{
|
|
ulongValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(ulong){0}", ulongValue));
|
|
case BasicDataType.Float:
|
|
if (decodedValue is not float floatValue)
|
|
{
|
|
floatValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(float){0}", floatValue));
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Double:
|
|
if (decodedValue is not double doubleValue)
|
|
{
|
|
doubleValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(double){0}", doubleValue));
|
|
case BasicDataType.String:
|
|
if (decodedValue is not string stringValue)
|
|
{
|
|
return MakeReturnType("default(string)");
|
|
}
|
|
return MakeReturnType(stringValue.AsStringLiteral());
|
|
case BasicDataType.DateTime:
|
|
if (decodedValue is DateTimeUtc dtutc)
|
|
{
|
|
if (dtutc.IsNull)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.DateTimeUtc.MinValue");
|
|
}
|
|
decodedValue = (DateTime)dtutc;
|
|
}
|
|
if (decodedValue is not DateTime dateTimeValue ||
|
|
dateTimeValue == DateTime.MinValue)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.DateTimeUtc.MinValue");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.DateTimeUtc.From(\"{0:yyyy-MM-dd HH:mm:ss}\")",
|
|
dateTimeValue));
|
|
case BasicDataType.Guid:
|
|
if (decodedValue is Uuid uuid)
|
|
{
|
|
decodedValue = uuid.Guid;
|
|
}
|
|
if (decodedValue is not Guid guidValue ||
|
|
guidValue == Guid.Empty)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.Uuid.Empty");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.Uuid.Parse(\"{0}\")",
|
|
guidValue));
|
|
case BasicDataType.ByteString:
|
|
if (decodedValue is ByteString byteStringValue)
|
|
{
|
|
decodedValue = byteStringValue.ToArray();
|
|
}
|
|
if (decodedValue is not byte[] byteArray)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.ByteString.Empty");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.ByteString.FromHexString(\"{0}\")",
|
|
CoreUtils.ToHexString(byteArray)));
|
|
case BasicDataType.NodeId:
|
|
if (decodedValue is not NodeId nodeId ||
|
|
nodeId.IsNull)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.NodeId.Null");
|
|
}
|
|
if (nodeId.NamespaceIndex == 0 ||
|
|
nodeId.NamespaceIndex >= namespaces.Length)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.NodeId.Parse(\"{0}\")",
|
|
nodeId));
|
|
}
|
|
var absoluteId = new ExpandedNodeId(
|
|
nodeId,
|
|
namespaces[nodeId.NamespaceIndex].Value);
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"ExpandedNodeId.Parse(\"{0}\", context.NamespaceUris)",
|
|
absoluteId));
|
|
case BasicDataType.ExpandedNodeId:
|
|
if (decodedValue is not ExpandedNodeId expandedNodeId ||
|
|
expandedNodeId.IsNull)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.ExpandedNodeId.Null");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.ExpandedNodeId.Parse(\"{0}\")",
|
|
expandedNodeId));
|
|
case BasicDataType.QualifiedName:
|
|
if (decodedValue is not QualifiedName qualifiedName ||
|
|
qualifiedName.IsNull)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.QualifiedName.Null");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.QualifiedName.Parse(\"{0}\")",
|
|
qualifiedName));
|
|
case BasicDataType.LocalizedText:
|
|
if (decodedValue is not Ua.LocalizedText localizedText ||
|
|
localizedText.IsNullOrEmpty)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.LocalizedText.Null");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"new global::Opc.Ua.LocalizedText({0}, {1})",
|
|
localizedText.Locale.AsStringLiteral(),
|
|
localizedText.Text.AsStringLiteral()));
|
|
case BasicDataType.StatusCode:
|
|
if (decodedValue is not StatusCode statusCode ||
|
|
statusCode.Code == 0)
|
|
{
|
|
return MakeReturnType("default(global::Opc.Ua.StatusCode)");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"(global::Opc.Ua.StatusCode.StatusCode){0}",
|
|
statusCode));
|
|
case BasicDataType.Enumeration:
|
|
if (dataType.BaseTypeNode?.SymbolicId ==
|
|
new XmlQualifiedName("OptionSet", Namespaces.OpcUa))
|
|
{
|
|
// OptionSet subtypes inherit from Opc.Ua.OptionSet (a
|
|
// class implementing IEncodeable), so Variant.From<T>
|
|
// (where T : struct, Enum) does not apply. Route them
|
|
// through Variant.FromStructure<T> (where T : IEncodeable).
|
|
return MakeReturnType(
|
|
CoreUtils.Format("new {0}()",
|
|
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces)),
|
|
"Structure");
|
|
}
|
|
if (!dataType.IsOptionSet && dataType.Fields?.Length > 0)
|
|
{
|
|
if (decodedValue is int enumValue)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format("({0}){1}",
|
|
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces),
|
|
enumValue));
|
|
}
|
|
if (decodedValue is null)
|
|
{
|
|
// Enum type declared - use first field as default
|
|
return MakeReturnType(CoreUtils.Format("{0}.{1}",
|
|
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces),
|
|
dataType.Fields[0].Name));
|
|
}
|
|
// TODO: change to initialize enum with the value
|
|
return onUnknownElement?.Invoke() ??
|
|
MakeReturnType(CoreUtils.Format("default({0})",
|
|
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces)));
|
|
}
|
|
// Default to enum with value 0 which should be "none" for option set
|
|
return onUnknownElement?.Invoke();
|
|
case BasicDataType.DataValue:
|
|
return MakeReturnType("new global::Opc.Ua.DataValue()");
|
|
case BasicDataType.UserDefined:
|
|
if (defaultValue == null)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"new {0}()",
|
|
GetDotNetTypeName(
|
|
dataType,
|
|
ValueRank.Scalar,
|
|
targetNamespace,
|
|
namespaces,
|
|
nullable: NullableAnnotation.NonNullable)),
|
|
"Structure");
|
|
}
|
|
return onUnknownElement?.Invoke();
|
|
default:
|
|
return onUnknownElement?.Invoke();
|
|
}
|
|
|
|
string MakeReturnType(string value, string prefix = null)
|
|
{
|
|
if (valueAsVariant)
|
|
{
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.From{0}({1})",
|
|
prefix ?? string.Empty,
|
|
value);
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get default for a array type
|
|
/// </summary>
|
|
internal static string GetArrayValueAsCode(
|
|
this DataTypeDesign dataType,
|
|
System.Xml.XmlElement defaultValue,
|
|
object decodedValue,
|
|
bool valueAsVariant,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
Func<string> onUnknownElement = null)
|
|
{
|
|
if ((decodedValue is not null &&
|
|
(decodedValue is not IList l || l.Count != 0) &&
|
|
(decodedValue is not Array a || a.Length != 0)) ||
|
|
(decodedValue is null && defaultValue is not null))
|
|
{
|
|
// TODO: change to initialize simple arrays with values
|
|
return onUnknownElement?.Invoke();
|
|
}
|
|
// Emit Array.Empty<T>() for empty arrays
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<bool>()");
|
|
case BasicDataType.SByte:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<sbyte>()");
|
|
case BasicDataType.Byte:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<byte>()");
|
|
case BasicDataType.Int16:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<short>()");
|
|
case BasicDataType.UInt16:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<ushort>()");
|
|
case BasicDataType.Int32:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<int>()");
|
|
case BasicDataType.UInt32:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<uint>()");
|
|
case BasicDataType.Int64:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<long>()");
|
|
case BasicDataType.UInt64:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<ulong>()");
|
|
case BasicDataType.Float:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<float>()");
|
|
case BasicDataType.Double:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<double>()");
|
|
case BasicDataType.String:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<string>()");
|
|
case BasicDataType.Guid:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.Uuid>()");
|
|
case BasicDataType.DateTime:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.DateTimeUtc>()");
|
|
case BasicDataType.ByteString:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.ByteString>()");
|
|
case BasicDataType.XmlElement:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.XmlElement>()");
|
|
case BasicDataType.NodeId:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.NodeId>()");
|
|
case BasicDataType.ExpandedNodeId:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.ExpandedNodeId>()");
|
|
case BasicDataType.QualifiedName:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.QualifiedName>()");
|
|
case BasicDataType.LocalizedText:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.LocalizedText>()");
|
|
case BasicDataType.StatusCode:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.StatusCode>()");
|
|
case BasicDataType.DataValue:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.DataValue>()");
|
|
case BasicDataType.Structure:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.ExtensionObject>()");
|
|
case BasicDataType.Enumeration:
|
|
case BasicDataType.UserDefined:
|
|
return MakeReturnType(
|
|
CoreUtils.Format(
|
|
"global::Opc.Ua.ArrayOf.Empty<{0}>()",
|
|
GetDotNetTypeName(
|
|
dataType,
|
|
ValueRank.Scalar,
|
|
targetNamespace,
|
|
namespaces,
|
|
nullable: NullableAnnotation.NonNullable)),
|
|
dataType.BasicDataType == BasicDataType.UserDefined ? "Structure" : null);
|
|
case BasicDataType.DiagnosticInfo:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.DiagnosticInfo>()");
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.Variant>()");
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
string MakeReturnType(string value, string prefix = null)
|
|
{
|
|
if (valueAsVariant)
|
|
{
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.From{0}({1})",
|
|
prefix ?? string.Empty,
|
|
value);
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get code string to load a variant from XML resource stream.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public static string GetVariantValueFromXmlAsCode(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
string xmlStreamResource,
|
|
string contextVariable = null)
|
|
{
|
|
contextVariable ??= "context";
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.BaseDataType:
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.FromXml({0}AsStream, {1})",
|
|
xmlStreamResource,
|
|
contextVariable);
|
|
case BasicDataType.UserDefined:
|
|
case BasicDataType.Structure:
|
|
case BasicDataType.Enumeration:
|
|
string dataTypeString = dataType.GetDotNetTypeName(
|
|
targetNamespace,
|
|
namespaces,
|
|
NullableAnnotation.NonNullable,
|
|
false);
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.FromXml({0}AsStream, {1}).Get{2}{3}<{4}>()",
|
|
xmlStreamResource,
|
|
contextVariable,
|
|
dataType.BasicDataType == BasicDataType.Enumeration ? "Enumeration" : "Structure",
|
|
valueRank == ValueRank.Array ? "Array" : string.Empty,
|
|
dataTypeString);
|
|
default:
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.FromXml({0}AsStream, {1}).Get{2}{3}()",
|
|
xmlStreamResource,
|
|
contextVariable,
|
|
dataType.BasicDataType.ToString(),
|
|
valueRank == ValueRank.Array ? "Array" : string.Empty);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the data type for a method argument.
|
|
/// </summary>
|
|
public static string GetMethodArgumentTypeAsCode(
|
|
this DataTypeDesign datatype,
|
|
ValueRank valueRank,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
bool isOptional)
|
|
{
|
|
string typeName = GetDotNetTypeName(
|
|
datatype,
|
|
targetNamespace,
|
|
namespaces,
|
|
// Make scalar optional nullable, but array/matrix elements non nullable
|
|
// TODO: Revisit this and make input non nullable but output parameter T
|
|
// nullable when we get rid of collections
|
|
nullable: isOptional && valueRank == ValueRank.Scalar ?
|
|
NullableAnnotation.Nullable : NullableAnnotation.NonNullable,
|
|
// Use raw arguments
|
|
false);
|
|
|
|
if (typeName is "global::Opc.Ua.IEncodeable" or "global::Opc.Ua.IEncodeable?")
|
|
|
|
{
|
|
typeName = "global::Opc.Ua.ExtensionObject";
|
|
}
|
|
if (typeName is "object" or "object?")
|
|
{
|
|
typeName = "global::Opc.Ua.Variant";
|
|
}
|
|
switch (valueRank)
|
|
{
|
|
case ValueRank.Scalar:
|
|
return typeName;
|
|
case ValueRank.Array:
|
|
return CoreUtils.Format("global::Opc.Ua.ArrayOf<{0}>", typeName);
|
|
case ValueRank.OneOrMoreDimensions:
|
|
return CoreUtils.Format("global::Opc.Ua.MatrixOf<{0}>", typeName);
|
|
default:
|
|
return "global::Opc.Ua.Variant";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the type is nullable
|
|
/// </summary>
|
|
/// <param name="type"></param>
|
|
/// <returns></returns>
|
|
public static bool IsXmlNillable(this BasicDataType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
case BasicDataType.SByte:
|
|
case BasicDataType.Byte:
|
|
case BasicDataType.Int16:
|
|
case BasicDataType.UInt16:
|
|
case BasicDataType.Int32:
|
|
case BasicDataType.UInt32:
|
|
case BasicDataType.Int64:
|
|
case BasicDataType.UInt64:
|
|
case BasicDataType.Float:
|
|
case BasicDataType.Double:
|
|
case BasicDataType.StatusCode:
|
|
case BasicDataType.Enumeration:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns system type for a basic data type.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string GetDotNetTypeName(
|
|
this DataTypeDesign datatype,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
NullableAnnotation nullable = NullableAnnotation.NonNullable,
|
|
bool useBuiltInWrapperTypes = false)
|
|
{
|
|
switch (datatype.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return "bool";
|
|
case BasicDataType.SByte:
|
|
return "sbyte";
|
|
case BasicDataType.Byte:
|
|
return "byte";
|
|
case BasicDataType.Int16:
|
|
return "short";
|
|
case BasicDataType.UInt16:
|
|
return "ushort";
|
|
case BasicDataType.Int32:
|
|
return "int";
|
|
case BasicDataType.UInt32:
|
|
return "uint";
|
|
case BasicDataType.Int64:
|
|
return "long";
|
|
case BasicDataType.UInt64:
|
|
return "ulong";
|
|
case BasicDataType.Float:
|
|
return "float";
|
|
case BasicDataType.Double:
|
|
return "double";
|
|
case BasicDataType.String:
|
|
return nullable == NullableAnnotation.NonNullable ?
|
|
"string" :
|
|
"string?";
|
|
case BasicDataType.DateTime:
|
|
return "global::Opc.Ua.DateTimeUtc";
|
|
case BasicDataType.Guid:
|
|
return "global::Opc.Ua.Uuid";
|
|
case BasicDataType.ByteString:
|
|
return "global::Opc.Ua.ByteString";
|
|
case BasicDataType.XmlElement:
|
|
return "global::Opc.Ua.XmlElement";
|
|
case BasicDataType.NodeId:
|
|
return "global::Opc.Ua.NodeId";
|
|
case BasicDataType.ExpandedNodeId:
|
|
return "global::Opc.Ua.ExpandedNodeId";
|
|
case BasicDataType.StatusCode:
|
|
return "global::Opc.Ua.StatusCode";
|
|
case BasicDataType.DiagnosticInfo:
|
|
return nullable == NullableAnnotation.NonNullable ?
|
|
"global::Opc.Ua.DiagnosticInfo" :
|
|
"global::Opc.Ua.DiagnosticInfo?";
|
|
case BasicDataType.QualifiedName:
|
|
return "global::Opc.Ua.QualifiedName";
|
|
case BasicDataType.LocalizedText:
|
|
return "global::Opc.Ua.LocalizedText";
|
|
case BasicDataType.DataValue:
|
|
return "global::Opc.Ua.DataValue";
|
|
case BasicDataType.Structure:
|
|
if (!useBuiltInWrapperTypes)
|
|
{
|
|
// return nullable == NullableAnnotation.NonNullable ?
|
|
// "global::Opc.Ua.IEncodeable" :
|
|
// "global::Opc.Ua.IEncodeable?";
|
|
}
|
|
return "global::Opc.Ua.ExtensionObject";
|
|
case BasicDataType.Enumeration:
|
|
if (datatype.SymbolicId ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
return "int";
|
|
}
|
|
|
|
if (datatype.IsOptionSet)
|
|
{
|
|
return GetDotNetTypeName(
|
|
(DataTypeDesign)datatype.BaseTypeNode,
|
|
targetNamespace,
|
|
namespaces,
|
|
nullable, // Should it always be non nullable?
|
|
useBuiltInWrapperTypes);
|
|
}
|
|
return datatype.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
case BasicDataType.UserDefined:
|
|
string typeName = datatype.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
if (datatype.IsEnumeration)
|
|
{
|
|
return typeName;
|
|
}
|
|
// All of these are always set to default type in properties when null
|
|
// is passed therefore we want to maintain non nullability no matter
|
|
return nullable != NullableAnnotation.Nullable ? typeName : typeName + "?";
|
|
}
|
|
if (!useBuiltInWrapperTypes)
|
|
{
|
|
return nullable == NullableAnnotation.NonNullable ? "object" : "object?";
|
|
}
|
|
return "global::Opc.Ua.Variant";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the type name for the data type with the provided value rank.
|
|
/// </summary>
|
|
public static string GetDotNetTypeName(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
NullableAnnotation nullable = NullableAnnotation.NonNullable,
|
|
bool useMatrixTypeInsteadOfVariant = false)
|
|
{
|
|
string typeName = dataType.GetDotNetTypeName(
|
|
targetNamespace,
|
|
namespaces,
|
|
// Make scalar optional nullable, but array/matrix elements non nullable
|
|
// TODO: Revisit this when we get rid of generated collections
|
|
valueRank == ValueRank.Scalar ? nullable : NullableAnnotation.NonNullable,
|
|
true);
|
|
if (typeName != null)
|
|
{
|
|
switch (valueRank)
|
|
{
|
|
case ValueRank.Scalar:
|
|
return typeName;
|
|
case ValueRank.Array:
|
|
return CoreUtils.Format("global::Opc.Ua.ArrayOf<{0}>", typeName);
|
|
case ValueRank.OneOrMoreDimensions when useMatrixTypeInsteadOfVariant:
|
|
return CoreUtils.Format("global::Opc.Ua.MatrixOf<{0}>", typeName);
|
|
}
|
|
}
|
|
return "global::Opc.Ua.Variant";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether a template parameter is required.
|
|
/// </summary>
|
|
public static bool IsTemplateParameterRequired(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank)
|
|
{
|
|
if (dataType.BasicDataType
|
|
is not BasicDataType.BaseDataType
|
|
and not BasicDataType.Number
|
|
and not BasicDataType.UInteger
|
|
and not BasicDataType.Integer &&
|
|
valueRank
|
|
is not ValueRank.OneOrMoreDimensions
|
|
and not ValueRank.ScalarOrOneDimension
|
|
and not ValueRank.ScalarOrArray)
|
|
{
|
|
return true;
|
|
}
|
|
if (valueRank == ValueRank.Array)
|
|
{
|
|
return true;
|
|
}
|
|
// Matrix-rank with a concrete data type that supports
|
|
// `MatrixOf<T>` is fully determinate (the generator produces
|
|
// `State<MatrixOf<T>>` rather than `State<T>`/`State<Variant>`),
|
|
// so the template parameter is in fact required to specialize
|
|
// the inherited State class.
|
|
if (valueRank == ValueRank.OneOrMoreDimensions &&
|
|
dataType.SupportsMatrixOf() &&
|
|
dataType.BasicDataType
|
|
is not BasicDataType.BaseDataType
|
|
and not BasicDataType.Number
|
|
and not BasicDataType.UInteger
|
|
and not BasicDataType.Integer)
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is overridden instance.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool IsOverridden(this InstanceDesign instance)
|
|
{
|
|
if (instance is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(instance));
|
|
}
|
|
return instance.OveriddenNode != null &&
|
|
instance.ModellingRule != ModellingRule.None &&
|
|
instance.ModellingRule != ModellingRule.ExposesItsArray &&
|
|
instance.ModellingRule != ModellingRule.MandatoryPlaceholder &&
|
|
instance.ModellingRule != ModellingRule.OptionalPlaceholder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the merged instance for an overriden node.
|
|
/// </summary>
|
|
public static InstanceDesign GetMergedInstance(this InstanceDesign instance)
|
|
{
|
|
for (NodeDesign parent = instance.Parent; parent != null; parent = parent.Parent)
|
|
{
|
|
if (parent.Parent == null && parent.Hierarchy != null)
|
|
{
|
|
string relativePath = instance.SymbolicId.Name;
|
|
|
|
int index = relativePath.IndexOf('_', StringComparison.Ordinal);
|
|
|
|
if (index != -1)
|
|
|
|
{
|
|
relativePath = relativePath[(index + 1)..];
|
|
}
|
|
if (parent.Hierarchy.Nodes.TryGetValue(relativePath,
|
|
out HierarchyNode hierarchyNode) &&
|
|
hierarchyNode.Instance is InstanceDesign instanceDesign)
|
|
{
|
|
return instanceDesign;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return instance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a name qualified with a namespace prefix.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static string GetPrefixedName(this XmlQualifiedName qname, List<string> namespaceUris)
|
|
{
|
|
if (qname.IsNull())
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
if (qname.Namespace == Namespaces.OpcUaBuiltInTypes)
|
|
{
|
|
return CoreUtils.Format("ua:{0}", qname.Name);
|
|
}
|
|
|
|
if (namespaceUris is null)
|
|
|
|
{
|
|
throw new ArgumentNullException(nameof(namespaceUris));
|
|
}
|
|
int index = namespaceUris.IndexOf(qname.Namespace);
|
|
|
|
if (index > 0)
|
|
{
|
|
return CoreUtils.Format("s{0}:{1}", index - 1, qname.Name);
|
|
}
|
|
|
|
return qname.Name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the data type to use for the value of a variable or the argument of a method.
|
|
/// </summary>
|
|
public static string GetBinaryDataType(
|
|
this DataTypeDesign dataType,
|
|
string targetNamespace,
|
|
Namespace[] namespaces)
|
|
{
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return "opc:Boolean";
|
|
case BasicDataType.SByte:
|
|
return "opc:SByte";
|
|
case BasicDataType.Byte:
|
|
return "opc:Byte";
|
|
case BasicDataType.Int16:
|
|
return "opc:Int16";
|
|
case BasicDataType.UInt16:
|
|
return "opc:UInt16";
|
|
case BasicDataType.Int32:
|
|
return "opc:Int32";
|
|
case BasicDataType.UInt32:
|
|
return "opc:UInt32";
|
|
case BasicDataType.Int64:
|
|
return "opc:Int64";
|
|
case BasicDataType.UInt64:
|
|
return "opc:UInt64";
|
|
case BasicDataType.Float:
|
|
return "opc:Float";
|
|
case BasicDataType.Double:
|
|
return "opc:Double";
|
|
case BasicDataType.String:
|
|
return "opc:CharArray";
|
|
case BasicDataType.DateTime:
|
|
return "opc:DateTime";
|
|
case BasicDataType.Guid:
|
|
return "opc:Guid";
|
|
case BasicDataType.ByteString:
|
|
return "opc:ByteString";
|
|
case BasicDataType.XmlElement:
|
|
return "ua:XmlElement";
|
|
case BasicDataType.NodeId:
|
|
return "ua:NodeId";
|
|
case BasicDataType.ExpandedNodeId:
|
|
return "ua:ExpandedNodeId";
|
|
case BasicDataType.StatusCode:
|
|
return "ua:StatusCode";
|
|
case BasicDataType.DiagnosticInfo:
|
|
return "ua:DiagnosticInfo";
|
|
case BasicDataType.QualifiedName:
|
|
return "ua:QualifiedName";
|
|
case BasicDataType.LocalizedText:
|
|
return "ua:LocalizedText";
|
|
case BasicDataType.DataValue:
|
|
return "ua:DataValue";
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType:
|
|
return "ua:Variant";
|
|
default:
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Structure", Namespaces.OpcUa))
|
|
{
|
|
return CoreUtils.Format("ua:ExtensionObject");
|
|
}
|
|
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetBinaryDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("opc:Int32");
|
|
}
|
|
|
|
string prefix = "tns";
|
|
|
|
if (dataType.SymbolicName.Namespace != targetNamespace)
|
|
{
|
|
if (dataType.SymbolicName.Namespace == Namespaces.OpcUa)
|
|
{
|
|
prefix = "ua";
|
|
}
|
|
else
|
|
{
|
|
prefix = GetXmlNamespacePrefix(
|
|
namespaces,
|
|
dataType.SymbolicName.Namespace);
|
|
}
|
|
}
|
|
return CoreUtils.Format("{0}:{1}", prefix, dataType.SymbolicName.Name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the data type to use for the value of a variable or the argument of a method.
|
|
/// </summary>
|
|
public static string GetXmlDataType(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank,
|
|
string targetNamespace,
|
|
Namespace[] namespaces)
|
|
{
|
|
if (valueRank != ValueRank.Scalar)
|
|
{
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return "ua:ListOfBoolean";
|
|
case BasicDataType.SByte:
|
|
return "ua:ListOfSByte";
|
|
case BasicDataType.Byte:
|
|
return "ua:ListOfByte";
|
|
case BasicDataType.Int16:
|
|
return "ua:ListOfInt16";
|
|
case BasicDataType.UInt16:
|
|
return "ua:ListOfUInt16";
|
|
case BasicDataType.Int32:
|
|
return "ua:ListOfInt32";
|
|
case BasicDataType.UInt32:
|
|
return "ua:ListOfUInt32";
|
|
case BasicDataType.Int64:
|
|
return "ua:ListOfInt64";
|
|
case BasicDataType.UInt64:
|
|
return "ua:ListOfUInt64";
|
|
case BasicDataType.Float:
|
|
return "ua:ListOfFloat";
|
|
case BasicDataType.Double:
|
|
return "ua:ListOfDouble";
|
|
case BasicDataType.String:
|
|
return "ua:ListOfString";
|
|
case BasicDataType.DateTime:
|
|
return "ua:ListOfDateTime";
|
|
case BasicDataType.Guid:
|
|
return "ua:ListOfGuid";
|
|
case BasicDataType.ByteString:
|
|
return "ua:ListOfByteString";
|
|
case BasicDataType.XmlElement:
|
|
return "ua:ListOfXmlElement";
|
|
case BasicDataType.NodeId:
|
|
return "ua:ListOfNodeId";
|
|
case BasicDataType.ExpandedNodeId:
|
|
return "ua:ListOfExpandedNodeId";
|
|
case BasicDataType.StatusCode:
|
|
return "ua:ListOfStatusCode";
|
|
case BasicDataType.DiagnosticInfo:
|
|
return "ua:ListOfDiagnosticInfo";
|
|
case BasicDataType.QualifiedName:
|
|
return "ua:ListOfQualifiedName";
|
|
case BasicDataType.LocalizedText:
|
|
return "ua:ListOfLocalizedText";
|
|
case BasicDataType.DataValue:
|
|
return "ua:ListOfDataValue";
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType:
|
|
return "ua:ListOfVariant";
|
|
default:
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Structure", Namespaces.OpcUa))
|
|
{
|
|
return CoreUtils.Format("ua:ListOfExtensionObject");
|
|
}
|
|
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetXmlDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode,
|
|
valueRank,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("ua:ListOfInt32");
|
|
}
|
|
|
|
string prefix = "tns";
|
|
|
|
if (dataType.SymbolicName.Namespace != targetNamespace)
|
|
{
|
|
if (dataType.SymbolicName.Namespace == Namespaces.OpcUa)
|
|
{
|
|
if (dataType.SymbolicName.Name == "Enumeration")
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetXmlDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode,
|
|
valueRank,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("ua:ListOfInt32");
|
|
}
|
|
|
|
prefix = "ua";
|
|
}
|
|
else
|
|
{
|
|
prefix = GetXmlNamespacePrefix(namespaces, dataType.SymbolicName.Namespace);
|
|
}
|
|
}
|
|
|
|
return CoreUtils.Format("{0}:ListOf{1}", prefix, dataType.SymbolicName.Name);
|
|
}
|
|
}
|
|
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return "xs:boolean";
|
|
case BasicDataType.SByte:
|
|
return "xs:byte";
|
|
case BasicDataType.Byte:
|
|
return "xs:unsignedByte";
|
|
case BasicDataType.Int16:
|
|
return "xs:short";
|
|
case BasicDataType.UInt16:
|
|
return "xs:unsignedShort";
|
|
case BasicDataType.Int32:
|
|
return "xs:int";
|
|
case BasicDataType.UInt32:
|
|
return "xs:unsignedInt";
|
|
case BasicDataType.Int64:
|
|
return "xs:long";
|
|
case BasicDataType.UInt64:
|
|
return "xs:unsignedLong";
|
|
case BasicDataType.Float:
|
|
return "xs:float";
|
|
case BasicDataType.Double:
|
|
return "xs:double";
|
|
case BasicDataType.String:
|
|
return "xs:string";
|
|
case BasicDataType.DateTime:
|
|
return "xs:dateTime";
|
|
case BasicDataType.Guid:
|
|
return "ua:Guid";
|
|
case BasicDataType.ByteString:
|
|
return "xs:base64Binary";
|
|
case BasicDataType.XmlElement:
|
|
return "ua:XmlElement";
|
|
case BasicDataType.NodeId:
|
|
return "ua:NodeId";
|
|
case BasicDataType.ExpandedNodeId:
|
|
return "ua:ExpandedNodeId";
|
|
case BasicDataType.StatusCode:
|
|
return "ua:StatusCode";
|
|
case BasicDataType.DiagnosticInfo:
|
|
return "ua:DiagnosticInfo";
|
|
case BasicDataType.QualifiedName:
|
|
return "ua:QualifiedName";
|
|
case BasicDataType.LocalizedText:
|
|
return "ua:LocalizedText";
|
|
case BasicDataType.DataValue:
|
|
return "ua:DataValue";
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType:
|
|
return "ua:Variant";
|
|
default:
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Structure", Namespaces.OpcUa))
|
|
{
|
|
return CoreUtils.Format("ua:ExtensionObject");
|
|
}
|
|
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetXmlDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode,
|
|
valueRank,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("xs:int");
|
|
}
|
|
|
|
string prefix = null;
|
|
|
|
if (dataType.SymbolicName.Namespace != targetNamespace)
|
|
{
|
|
if (dataType.SymbolicName.Namespace == Namespaces.OpcUa)
|
|
{
|
|
if (dataType.SymbolicName.Name == "Enumeration")
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetXmlDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode,
|
|
valueRank,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("xs:int");
|
|
}
|
|
|
|
prefix = "ua";
|
|
}
|
|
else
|
|
{
|
|
prefix = GetXmlNamespacePrefix(namespaces, dataType.SymbolicName.Namespace);
|
|
}
|
|
}
|
|
|
|
return CoreUtils.Format("{0}:{1}", prefix ?? "tns", dataType.SymbolicName.Name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a constant for the namespace uri.
|
|
/// </summary>
|
|
public static Namespace GetNamespace(this Namespace[] namespaces, string namespaceUri)
|
|
{
|
|
if (namespaces != null)
|
|
{
|
|
foreach (Namespace ns in namespaces)
|
|
{
|
|
if (ns.Value == namespaceUri)
|
|
{
|
|
return ns;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a constant for the namespace uri.
|
|
/// </summary>
|
|
public static string GetConstantForXmlNamespace(this Namespace[] namespaces, string namespaceUri)
|
|
{
|
|
Namespace ns = GetNamespace(namespaces, namespaceUri);
|
|
if (ns != null)
|
|
{
|
|
if (!string.IsNullOrEmpty(ns.XmlNamespace))
|
|
{
|
|
return CoreUtils.Format("{1}.Namespaces.{0}Xsd", ns.Name, ns.Prefix);
|
|
}
|
|
|
|
return CoreUtils.Format("{1}.Namespaces.{0}", ns.Name, ns.Prefix);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the XML prefix for the specified namespace.
|
|
/// </summary>
|
|
public static string GetXmlNamespacePrefix(this Namespace[] namespaces, string namespaceUri)
|
|
{
|
|
if (namespaceUri == null)
|
|
{
|
|
return null;
|
|
}
|
|
if (namespaces != null)
|
|
{
|
|
for (int ii = 0; ii < namespaces.Length; ii++)
|
|
{
|
|
if (namespaces[ii].Value == namespaceUri)
|
|
{
|
|
if (string.IsNullOrEmpty(namespaces[ii].XmlPrefix))
|
|
{
|
|
return CoreUtils.Format("s{0}", ii);
|
|
}
|
|
|
|
return namespaces[ii].XmlPrefix;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static bool IsMethodTypeDesign(this NodeDesign node)
|
|
{
|
|
if (node?.SymbolicId is null)
|
|
{
|
|
return false;
|
|
}
|
|
if (node is not MethodDesign)
|
|
|
|
{
|
|
return false;
|
|
}
|
|
string symbol = node.SymbolicId.Name;
|
|
|
|
int index = symbol.IndexOf('_', StringComparison.Ordinal);
|
|
|
|
if (index > 0)
|
|
|
|
{
|
|
symbol = symbol[..index];
|
|
}
|
|
return symbol.EndsWith("MethodType", StringComparison.Ordinal);
|
|
}
|
|
|
|
public static bool IsPartOfOpcUaTypesLibrary(this DataTypeDesign dataType)
|
|
{
|
|
if (dataType != null &&
|
|
dataType.SymbolicId.Namespace == Namespaces.OpcUa)
|
|
{
|
|
switch (dataType.NumericId)
|
|
{
|
|
case DataTypes.AccessRestrictionType:
|
|
case DataTypes.ReferenceDescription:
|
|
case DataTypes.AttributeWriteMask:
|
|
case DataTypes.Argument:
|
|
case DataTypes.IdType:
|
|
case DataTypes.RolePermissionType:
|
|
case DataTypes.PermissionType:
|
|
case DataTypes.ViewDescription:
|
|
case DataTypes.BrowseDescription:
|
|
case DataTypes.StructureDefinition:
|
|
case DataTypes.StructureType:
|
|
case DataTypes.StructureField:
|
|
case DataTypes.InstanceNode:
|
|
case DataTypes.ReferenceTypeNode:
|
|
case DataTypes.ReferenceNode:
|
|
case DataTypes.DataTypeDefinition:
|
|
case DataTypes.EnumDefinition:
|
|
case DataTypes.EnumField:
|
|
case DataTypes.EnumValueType:
|
|
case DataTypes.RelativePath:
|
|
case DataTypes.BrowseDirection:
|
|
case DataTypes.RelativePathElement:
|
|
case DataTypes.NodeClass:
|
|
case DataTypes.Node:
|
|
case DataTypes.ViewNode:
|
|
case DataTypes.ObjectNode:
|
|
case DataTypes.MethodNode:
|
|
case DataTypes.TypeNode:
|
|
case DataTypes.ObjectTypeNode:
|
|
case DataTypes.DataTypeNode:
|
|
case DataTypes.VariableTypeNode:
|
|
case DataTypes.VariableNode:
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static Export.ReleaseStatus ToNodeSetReleaseStatus(
|
|
this ReleaseStatus releaseStatus)
|
|
{
|
|
switch (releaseStatus)
|
|
{
|
|
case ReleaseStatus.Deprecated:
|
|
return Export.ReleaseStatus.Deprecated;
|
|
case ReleaseStatus.RC:
|
|
case ReleaseStatus.Draft:
|
|
return Export.ReleaseStatus.Draft;
|
|
default:
|
|
return Export.ReleaseStatus.Released;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the access level enumeration onto a code string.
|
|
/// </summary>
|
|
public static string GetAccessLevelAsCode(this AccessLevel accessLevel)
|
|
{
|
|
return accessLevel switch
|
|
{
|
|
AccessLevel.Read => "global::Opc.Ua.AccessLevels.CurrentRead",
|
|
AccessLevel.Write => "global::Opc.Ua.AccessLevels.CurrentWrite",
|
|
AccessLevel.ReadWrite => "global::Opc.Ua.AccessLevels.CurrentReadOrWrite",
|
|
AccessLevel.HistoryRead => "global::Opc.Ua.AccessLevels.HistoryRead",
|
|
AccessLevel.HistoryWrite => "global::Opc.Ua.AccessLevels.HistoryWrite",
|
|
AccessLevel.HistoryReadWrite => "global::Opc.Ua.AccessLevels.HistoryReadOrWrite",
|
|
_ => "global::Opc.Ua.AccessLevels.None"
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the AccessLevel of a variable onto code. NodeSet2-sourced
|
|
/// designs carry the verbatim bitmask, which is preferred because
|
|
/// the ModelDesign <see cref="AccessLevel"/> enumeration cannot
|
|
/// represent combinations such as <c>CurrentRead | HistoryRead</c>.
|
|
/// </summary>
|
|
public static string GetAccessLevelAsCode(this VariableDesign variable)
|
|
{
|
|
uint? rawAccessLevel = variable?.RawAccessLevel;
|
|
return rawAccessLevel != null
|
|
? GetAccessLevelBitsAsCode(rawAccessLevel.Value)
|
|
: GetAccessLevelAsCode(variable?.AccessLevel ?? AccessLevel.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the UserAccessLevel of a variable onto code. Mirrors the
|
|
/// AccessLevel, matching the runtime NodeSet2 importer in
|
|
/// <c>UANodeSetHelpers</c>, which derives UserAccessLevel from
|
|
/// AccessLevel rather than from the (schema-defaulted)
|
|
/// UserAccessLevel attribute.
|
|
/// </summary>
|
|
public static string GetUserAccessLevelAsCode(this VariableDesign variable)
|
|
{
|
|
return GetAccessLevelAsCode(variable);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a verbatim OPC UA AccessLevel bitmask onto code, emitting
|
|
/// the named <c>AccessLevels</c> constants for every set bit.
|
|
/// </summary>
|
|
private static string GetAccessLevelBitsAsCode(uint accessLevel)
|
|
{
|
|
byte bits = (byte)accessLevel;
|
|
if (bits == 0)
|
|
{
|
|
return "global::Opc.Ua.AccessLevels.None";
|
|
}
|
|
|
|
var names = new List<string>();
|
|
AppendBit(names, bits, 0x1, "CurrentRead");
|
|
AppendBit(names, bits, 0x2, "CurrentWrite");
|
|
AppendBit(names, bits, 0x4, "HistoryRead");
|
|
AppendBit(names, bits, 0x8, "HistoryWrite");
|
|
AppendBit(names, bits, 0x10, "SemanticChange");
|
|
AppendBit(names, bits, 0x20, "StatusWrite");
|
|
AppendBit(names, bits, 0x40, "TimestampWrite");
|
|
|
|
if (names.Count == 0)
|
|
{
|
|
return CoreUtils.Format("(byte)0x{0:X2}", bits);
|
|
}
|
|
if (names.Count == 1)
|
|
{
|
|
return names[0];
|
|
}
|
|
return CoreUtils.Format("(byte)({0})", string.Join(" | ", names));
|
|
}
|
|
|
|
private static void AppendBit(List<string> names, byte bits, byte mask, string name)
|
|
{
|
|
if ((bits & mask) != 0)
|
|
{
|
|
names.Add("global::Opc.Ua.AccessLevels." + name);
|
|
}
|
|
}
|
|
|
|
public static string GetLocalizedTextAsCode(this string localizedText)
|
|
{
|
|
return GetLocalizedTextAsCode(new LocalizedText
|
|
{
|
|
Value = localizedText
|
|
}, noAutoGen: false);
|
|
}
|
|
|
|
public static string GetLocalizedTextAsCode(this LocalizedText localizedText, bool noAutoGen = false)
|
|
{
|
|
if (localizedText?.Value != null && (!noAutoGen || !localizedText.IsAutogenerated))
|
|
{
|
|
return CoreUtils.Format(
|
|
"new global::Opc.Ua.LocalizedText({0}, string.Empty, {1})",
|
|
localizedText.Key.AsStringLiteral(),
|
|
localizedText.Value.Trim().AsStringLiteral());
|
|
}
|
|
return "global::Opc.Ua.LocalizedText.Null";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the array dimensions onto code that creates the array.
|
|
/// </summary>
|
|
public static string GetArrayDimensionsAsCode(this ValueRank valueRank, string arrayDimensions)
|
|
{
|
|
if (valueRank is < 0 and not ValueRank.OneOrMoreDimensions)
|
|
{
|
|
return null;
|
|
}
|
|
if (string.IsNullOrEmpty(arrayDimensions))
|
|
{
|
|
if (valueRank == ValueRank.Array)
|
|
{
|
|
return "new uint[] { 0 }";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
var tokens = arrayDimensions
|
|
.Split([','], StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(t => t.Trim())
|
|
.Where(t => t.Length > 0)
|
|
.ToList();
|
|
|
|
if (tokens.Count == 0)
|
|
|
|
{
|
|
return null;
|
|
}
|
|
return CoreUtils.Format(
|
|
"new uint[] {{ {0} }}",
|
|
string.Join(", ", tokens.Select(t =>
|
|
{
|
|
if (uint.TryParse(t, out uint val))
|
|
{
|
|
return val.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
return "0";
|
|
})));
|
|
}
|
|
|
|
public static string GetAccessRestrictionsAsCode(
|
|
this AccessRestrictions restrictions,
|
|
bool restrictionsSpecified)
|
|
{
|
|
if (!restrictionsSpecified)
|
|
{
|
|
return null;
|
|
}
|
|
return restrictions switch
|
|
{
|
|
AccessRestrictions.SigningRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SigningRequired",
|
|
AccessRestrictions.EncryptionRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired",
|
|
AccessRestrictions.SessionRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired",
|
|
AccessRestrictions.SessionWithSigningRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SigningRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired",
|
|
AccessRestrictions.SessionWithEncryptionRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired",
|
|
AccessRestrictions.SigningAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SigningRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.EncryptionAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.SessionAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.SessionWithSigningAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SigningRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.SessionWithEncryptionAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
public static string GetPermissionTypeAsCode(this Permissions[] permissions)
|
|
{
|
|
if (permissions == null || permissions.Length == 0)
|
|
{
|
|
return "global::Opc.Ua.PermissionType.None";
|
|
}
|
|
var parts = new HashSet<string>();
|
|
foreach (Permissions p in permissions)
|
|
{
|
|
switch (p)
|
|
{
|
|
case Permissions.All:
|
|
AddPermissionStrings(Permissions.WriteAttribute, parts);
|
|
AddPermissionStrings(Permissions.WriteRolePermissions, parts);
|
|
AddPermissionStrings(Permissions.WriteHistorizing, parts);
|
|
AddPermissionStrings(Permissions.Write, parts);
|
|
AddPermissionStrings(Permissions.InsertHistory, parts);
|
|
AddPermissionStrings(Permissions.ModifyHistory, parts);
|
|
AddPermissionStrings(Permissions.DeleteHistory, parts);
|
|
AddPermissionStrings(Permissions.Call, parts);
|
|
AddPermissionStrings(Permissions.AddReference, parts);
|
|
AddPermissionStrings(Permissions.RemoveReference, parts);
|
|
AddPermissionStrings(Permissions.DeleteNode, parts);
|
|
AddPermissionStrings(Permissions.AddNode, parts);
|
|
goto case Permissions.AllRead;
|
|
case Permissions.AllRead:
|
|
AddPermissionStrings(Permissions.Browse, parts);
|
|
AddPermissionStrings(Permissions.Read, parts);
|
|
AddPermissionStrings(Permissions.ReadHistory, parts);
|
|
AddPermissionStrings(Permissions.ReceiveEvents, parts);
|
|
AddPermissionStrings(Permissions.ReadRolePermissions, parts);
|
|
break;
|
|
default:
|
|
AddPermissionStrings(p, parts);
|
|
break;
|
|
}
|
|
}
|
|
return parts.Count > 0 ?
|
|
string.Join(" | ", parts) :
|
|
"global::Opc.Ua.PermissionType.None";
|
|
|
|
static void AddPermissionStrings(Permissions p, HashSet<string> parts)
|
|
{
|
|
string part = p switch
|
|
{
|
|
Permissions.Browse =>
|
|
"global::Opc.Ua.PermissionType.Browse",
|
|
Permissions.Read =>
|
|
"global::Opc.Ua.PermissionType.Read",
|
|
Permissions.Write =>
|
|
"global::Opc.Ua.PermissionType.Write",
|
|
Permissions.Call =>
|
|
"global::Opc.Ua.PermissionType.Call",
|
|
Permissions.ReadHistory =>
|
|
"global::Opc.Ua.PermissionType.ReadHistory",
|
|
Permissions.InsertHistory =>
|
|
"global::Opc.Ua.PermissionType.InsertHistory",
|
|
Permissions.ModifyHistory =>
|
|
"global::Opc.Ua.PermissionType.ModifyHistory",
|
|
Permissions.DeleteHistory =>
|
|
"global::Opc.Ua.PermissionType.DeleteHistory",
|
|
Permissions.ReceiveEvents =>
|
|
"global::Opc.Ua.PermissionType.ReceiveEvents",
|
|
Permissions.AddNode =>
|
|
"global::Opc.Ua.PermissionType.AddNode",
|
|
Permissions.DeleteNode =>
|
|
"global::Opc.Ua.PermissionType.DeleteNode",
|
|
Permissions.AddReference =>
|
|
"global::Opc.Ua.PermissionType.AddReference",
|
|
Permissions.RemoveReference =>
|
|
"global::Opc.Ua.PermissionType.RemoveReference",
|
|
Permissions.ReadRolePermissions =>
|
|
"global::Opc.Ua.PermissionType.ReadRolePermissions",
|
|
Permissions.WriteRolePermissions =>
|
|
"global::Opc.Ua.PermissionType.WriteRolePermissions",
|
|
Permissions.WriteAttribute =>
|
|
"global::Opc.Ua.PermissionType.WriteAttribute",
|
|
Permissions.WriteHistorizing =>
|
|
"global::Opc.Ua.PermissionType.WriteHistorizing",
|
|
_ => null
|
|
};
|
|
if (part != null)
|
|
{
|
|
parts.Add(part);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static string GetNodeIdAsCode(
|
|
this NodeDesign node,
|
|
Namespace[] namespaces,
|
|
string namespaceTableVariable)
|
|
{
|
|
string identifier;
|
|
if (node == null)
|
|
{
|
|
return "global::Opc.Ua.NodeId.Null";
|
|
}
|
|
if (node.NumericIdSpecified)
|
|
{
|
|
identifier = CoreUtils.Format("{0}u", node.NumericId);
|
|
}
|
|
else if (!string.IsNullOrEmpty(node.StringId))
|
|
{
|
|
identifier = node.StringId.AsStringLiteral();
|
|
}
|
|
else
|
|
{
|
|
uint? numericId = FindNumericIdentifier(node);
|
|
if (numericId.HasValue)
|
|
{
|
|
identifier = CoreUtils.Format("{0}u", numericId.Value);
|
|
}
|
|
else
|
|
{
|
|
if (string.IsNullOrEmpty(node.SymbolicId.Name))
|
|
{
|
|
return "global::Opc.Ua.NodeId.Null";
|
|
}
|
|
identifier = node.SymbolicId.Name.AsStringLiteral();
|
|
}
|
|
}
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.NodeId.Create({0}, {1}, {2})",
|
|
identifier,
|
|
namespaces.GetConstantSymbolForNamespace(
|
|
node.SymbolicId.Namespace),
|
|
namespaceTableVariable);
|
|
}
|
|
|
|
public static string GetExpandedNodeIdAsCode(
|
|
this NodeDesign node,
|
|
Namespace[] namespaces)
|
|
{
|
|
string identifier;
|
|
if (node == null)
|
|
{
|
|
return "global::Opc.Ua.ExpandedNodeId.Null";
|
|
}
|
|
if (node.NumericIdSpecified)
|
|
{
|
|
identifier = CoreUtils.Format("{0}u", node.NumericId);
|
|
}
|
|
else if (!string.IsNullOrEmpty(node.StringId))
|
|
{
|
|
identifier = node.StringId.AsStringLiteral();
|
|
}
|
|
else
|
|
{
|
|
uint? numericId = FindNumericIdentifier(node);
|
|
if (numericId.HasValue)
|
|
{
|
|
identifier = CoreUtils.Format("{0}u", numericId.Value);
|
|
}
|
|
else
|
|
{
|
|
if (string.IsNullOrEmpty(node.SymbolicId.Name))
|
|
{
|
|
return "global::Opc.Ua.ExpandedNodeId.Null";
|
|
}
|
|
identifier = node.SymbolicId.Name.AsStringLiteral();
|
|
}
|
|
}
|
|
return CoreUtils.Format(
|
|
"new global::Opc.Ua.ExpandedNodeId({0}, {1})",
|
|
identifier,
|
|
namespaces.GetConstantSymbolForNamespace(
|
|
node.SymbolicId.Namespace));
|
|
}
|
|
|
|
public static uint? FindNumericIdentifier(this NodeDesign node)
|
|
{
|
|
uint? numericId = node.NumericIdSpecified ? node.NumericId : null;
|
|
for (NodeDesign parent = node.Parent;
|
|
!numericId.HasValue && parent != null;
|
|
parent = parent.Parent)
|
|
{
|
|
if (parent.Hierarchy == null)
|
|
{
|
|
continue;
|
|
}
|
|
string browsePath = node.SymbolicId.Name;
|
|
string parentPath = parent.SymbolicId.Name;
|
|
if (browsePath.StartsWith(parentPath, StringComparison.InvariantCulture) &&
|
|
browsePath[parentPath.Length] == NodeDesign.PathChar)
|
|
{
|
|
browsePath = browsePath[(parentPath.Length + 1)..];
|
|
}
|
|
|
|
if (parent.Hierarchy.Nodes.TryGetValue(browsePath, out HierarchyNode instance) &&
|
|
instance.Instance.NumericId != 0)
|
|
{
|
|
numericId = instance.Instance.NumericId;
|
|
}
|
|
}
|
|
return numericId;
|
|
}
|
|
|
|
public static string GetNodeIdConstant(
|
|
this IModelDesign design,
|
|
XmlQualifiedName symbolidId,
|
|
string type,
|
|
string namespaceTableVariable)
|
|
{
|
|
if (symbolidId == null)
|
|
{
|
|
return "global::Opc.Ua.NodeId.Null";
|
|
}
|
|
NodeDesign node = design.FindNode(
|
|
symbolidId,
|
|
symbolidId.Name,
|
|
type);
|
|
if (node == null)
|
|
{
|
|
return "global::Opc.Ua.NodeId.Null";
|
|
}
|
|
return node.GetNodeIdAsCode(design.Namespaces, namespaceTableVariable);
|
|
}
|
|
|
|
public static void SetDefaultValue(
|
|
this VariableTypeDesign node,
|
|
object value,
|
|
IServiceMessageContext context)
|
|
{
|
|
if (value != null)
|
|
{
|
|
node.DefaultValue = AsXmlElement(value, context);
|
|
node.DecodedValue = value;
|
|
}
|
|
else
|
|
{
|
|
node.DefaultValue = null;
|
|
node.DecodedValue = null;
|
|
}
|
|
}
|
|
|
|
public static void SetDefaultValue(
|
|
this VariableDesign node,
|
|
object value,
|
|
IServiceMessageContext context)
|
|
{
|
|
if (value != null)
|
|
{
|
|
node.DefaultValue = AsXmlElement(value, context);
|
|
node.DecodedValue = value;
|
|
}
|
|
else
|
|
{
|
|
node.DefaultValue = null;
|
|
node.DecodedValue = null;
|
|
}
|
|
}
|
|
|
|
private static System.Xml.XmlElement AsXmlElement(
|
|
object value,
|
|
IServiceMessageContext context)
|
|
{
|
|
if (VariantHelper.TryCastFromWithReflectionFallback(value, out Variant variant) &&
|
|
!variant.IsNull)
|
|
{
|
|
using var encoder = new XmlEncoder(context);
|
|
encoder.WriteVariantValue(null, variant);
|
|
var xmlDoc = new XmlDocument();
|
|
xmlDoc.LoadInnerXml(encoder.CloseAndReturnText());
|
|
return xmlDoc.DocumentElement;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static bool IsBasicDataType(
|
|
this DataTypeDesign dataType,
|
|
out BasicDataType basicDataType)
|
|
{
|
|
if (dataType.SymbolicName.Namespace == "http://opcfoundation.org/UA/")
|
|
{
|
|
#if NET8_0_OR_GREATER
|
|
foreach (string name in Enum.GetNames<BasicDataType>())
|
|
#else
|
|
foreach (string name in Enum.GetNames(typeof(BasicDataType)))
|
|
#endif
|
|
{
|
|
if (name == dataType.SymbolicName.Name)
|
|
{
|
|
#if NET8_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
|
|
basicDataType = Enum.Parse<BasicDataType>(
|
|
dataType.SymbolicName.Name);
|
|
#else
|
|
basicDataType = (BasicDataType)Enum.Parse(
|
|
typeof(BasicDataType),
|
|
dataType.SymbolicName.Name);
|
|
#endif
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
basicDataType = default;
|
|
return false;
|
|
}
|
|
}
|
|
}
|