mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-19 07:02:16 +08:00
397db603db
# Description Implements the **Asset Administration Shell** companion specifications on top of `marcschier/wot-type-binding`, in two halves that a Server chooses between: * **AAS V3** — the [draft](https://github.com/marcschier/opcua-drafts/tree/main/companion-specs/AAS) (`http://opcfoundation.org/UA/I4AAS/v3/`), all sixteen conformance units: the metamodel, the registry built on the abstract xRegistry base, generational materialization, federation, packages with digest-bound integrity, the Digital Product Passport vocabulary, and the Annex F bridge to WoT Thing Descriptions. * **AAS V2** — the published [OPC 30270](https://reference.opcfoundation.org/specs/OPC-30270/full) (`http://opcfoundation.org/UA/I4AAS/`), which maps the AAS V2.0.1 metamodel: object model, ingestion of XML, JSON and AASX, materialization, server and client. The two are different metamodel generations rather than two revisions of one model — V2 keeps `Asset` as a first-class Identifiable, `identification.idType`, Views and ordered collections, none of which survive into V3 — so they are **alternatives**. A Server hosts one or the other; this is documented rather than enforced at startup. Each is named for the generation it maps (`Opc.Ua.Aas.V2`, `Opc.Ua.Aas.V3`, `AddAasV2Server`, `AddAasV3Server`), and what genuinely serves both carries no marker and sits at the root of `Opc.Ua.Aas`: the deterministic NodeId encoding and idShortPath conventions, `AasOptional<T>`, the digest rule, the materialization diagnostics, the AASX container walk, and the value and operation providers, which are keyed on `NodeId` and `Variant` and so carry no version at all. ## What is in here **New libraries**: `Opc.Ua.Aas`, `Opc.Ua.Aas.Server`, `Opc.Ua.Aas.Client`, `Opc.Ua.Aas.WoT`, plus `samples/Aas` and `docs/Aas.md`. **Enabling changes to existing code**, each needed by the above: * The OPC UA `Decimal` DataType added to `Opc.Ua.Types`, with its Part 6 §5.1.10 wire encoding. `BinaryDecoder` gained one internal member, `TryReadRemainingBodyBytes`, because a trailing field with no self-describing length cannot otherwise be read; `Decimal.Decode` is its only caller. Part 6 Table 3 types the unscaled `Value` as an **OctetString whose byte count is `Length` − 2**, not a length-prefixed `ByteString`, so it carries no length of its own — `DecimalEncodingTests` pins the body byte-for-byte against that mistake, and against the `Length ≤ 2` values the same table calls invalid. * The xRegistry projection engine extracted into `Opc.Ua.XRegistry.Server` and `Opc.Ua.WotCon.Server` migrated onto it (−848/+387 lines), with the WoT Connectivity tests passing **unedited**. * Two source-generator fixes, both of which made a model impossible to build (see below). ## Defects found and fixed along the way Running the stack end to end, rather than trusting per-area test suites, surfaced defects that no unit test caught. The ones worth a maintainer's attention: * **The generator emitted non-terminating constructors.** A structure field with no declared default was initialized with `new T()`, so a self-referential DataType (`AASReferenceDataType.referredSemanticId`) recursed until the stack overflowed and `AddOpcUaAas` could not load the model at all. The test host *died* rather than failing. Only the field that closes a cycle is now left null. * **The generator omitted `new` on a redeclared placeholder.** `AASOrderedSubmodelElementCollectionType` redeclares the `SubmodelElement` slot of its base type, and the generated `Add` method hid the base one without saying so, so the V2 model would not compile. No existing model could have hit either defect — both are build failures, so anything that builds today is unaffected. * **The metamodel half had never worked outside its unit tests**: the emitted NodeSet declared no reference-type aliases and was rejected by the loader; the Mandatory `Invoke` Method was never emitted, because a NodeSet is imported as written rather than instantiated from its type; the runtime bound at the authored namespace index instead of the one the Server assigns; and `MethodDeclarationId` was addressed in the wrong namespace. Every materializer test inspected the in-memory object graph, which cannot see whether the graph is loadable — two tests now import it exactly as `RuntimeNodeSetNodeManager` does. * **The client could not see any Property member.** It browsed `HasComponent` with subtypes, but `HasProperty` is a sibling under `Aggregates`, so reading a value failed and invoking failed argument validation. * **Every repeated registry write forked a duplicate group and resource** instead of adding a version, because the identifier was re-derived against a sibling set that already contained the entity itself. This also made the persistence bounds unreachable. * `.gitignore`'s `**/packages/*` silently swallowed the whole package-integrity implementation and its tests, which would have vanished on a fresh clone. ## Specification findings Nine findings against the V3 draft are fixed in [marcschier/opcua-drafts#75](https://github.com/marcschier/opcua-drafts/pull/75). The substantive one: clause 6.1.5's absent-versus-empty rule was **unsatisfiable** for five container fields that materialize as parent components with no node of their own, which made `AAS-LosslessRoundTrip` unachievable. OPC 30270 contradicts itself twice. Both are recorded in `AasV2ConformanceUnits.cs` and the documentation rather than smoothed over: Table 83 spells the unit `I4AAS MultiLangaugeProperty` where Table 85 spells it correctly, and Table 85 lists an `I4AAS Security` unit that Table 83 never defines. Table 84, which would assign profile URIs, is empty, so `ServerProfileArray` gains no I4AAS entry. ## Known limitation Calling an `Operation` **with arguments** answers `BadTooManyArguments`. Method resolution works from both the instance NodeId and the `MethodDeclarationId`, and the argument definitions are published, but `NodeState.AddChild` does not route through `MethodState.FindChild`, so a Method imported from a NodeSet leaves its typed `InputArguments` null. Closing this means changing how the shared NodeSet import claims typed children, which affects every consumer of runtime NodeSets and is out of scope here. It is documented in `docs/Aas.md` §11, the conformance matrix marks `AAS-OperationInvoke` partial rather than claiming it, and `samples/Aas` exercises the path and reports the status rather than working around it. V2 is unaffected — its `Operation` is an OptionalPlaceholder with no declared arguments. ## Related Issues - No tracking issue yet. Opening as a **draft** for design discussion first, per the ADR guidance below; happy to open one and link it once the shape is agreed. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. ### Verification | Check | Result | |---|---| | AAS tests | **670 passed, 0 failed** on net472, net48, net8.0, net9.0, net10.0 | | Coverage | Client 80.8 %, Server 81.2 %, `Opc.Ua.Aas` 82.0 %, WoT 82.4 % — all above the 80 % gate | | Libraries | `Opc.Ua.Aas`, `.Server`, `.Client`, `.WoT` build clean on every target framework, zero warnings | | Regression | `Opc.Ua.Types` 8677, `Opc.Ua.WotCon` 1094, `Opc.Ua.XRegistry` 198, `Opc.Ua.Server` 4050 — all green | | Sample | `samples/Aas` builds warning-free and runs end to end | | Merge validation | Solution builds clean on net10.0 and net48; WoT Types 504, WotCon 1111, AAS 670, XRegistry 198 tests passed | | CI | All 99 checks green on `ff9298e44`; only the CLA signature status is outstanding | ### Review pass A full review against both specifications, and for API consistency and correctness, produced twelve fixes. Each one has a test that fails without it. | Defect | Where | Why it mattered | |---|---|---| | V2 projection aborted on **every** `File` read from a document | `AasV2EnvironmentRuntime` | The runtime bound `File/Value` unconditionally, the materializer emitted it only with content, and the builder throws `BadNotFound` for an unknown NodeId — aborting the whole registration. No reader ever populates `AasFile.File`, so no AAS V2 document could be hosted at all. | | `aas-suppl` anchored on the wrong part | `AasxPackageReaderCore`, `AasxPackageWriter` | IDTA and the Package Explorer anchor it on the environment part; we used the origin part. Every real `.aasx` returned zero supplementary files, silently. Affects both halves. | | Federation failed closed on pooled connections | `AasFederation` | `ConnectCallback` fires only for new connections, so the second fetch to an origin — and all of net472/net48/netstandard2.1 — failed with a security-flavoured error. The peer is now validated *inside* the callback, which makes reuse safe by construction. | | Malformed `Decimal` escaped as `ArgumentOutOfRangeException` | `BinaryDecoder` | A declared body shorter than `Scale` produced a negative read length. Two of the three test cases had been passing for the wrong reason. | | V2 and V3 cross-wired their value providers | `OpcUaAasServerBuilderExtensions` | Both registered `IAasValueProvider`; whichever was added first served both AddressSpaces. V2 now has its own contract, so co-hosting is harmless rather than silently wrong. `EnvironmentFolder` was also bound and ignored on the V2 path. | | Export filter skipped three nesting kinds | `AasEnvironmentExporter` | Content under `Entity`, `AnnotatedRelationshipElement` and `Operation` was exported unevaluated *and* stamped with a Digest presenting it as complete (clause 6.5.10). List authorization paths also shifted after a denial. | | Concealment held at the Method but not in the projection | `AasRegistryModel` | `GetSubmodel` answered `BadNotFound` while an anonymous Browse revealed the node, its identifier, semanticId and digest (clause 6.5.7). | | File handles were unsynchronized and unowned | `AasElementFileManager` | The async node manager holds no lock, so concurrent Calls raced a bare `Dictionary`; sequential handles let any Session read, seek or commit another's. Writes were also unbounded. | | Projection host was unimplementable outside the stack | `AasEnvironmentProjection` | The handle demanded a type with an internal constructor — our own tests used `FormatterServices.GetUninitializedObject` to get round it. Now mirrors WotCon's interface-typed registration. | | Embedded `FileType` omitted 7 of 10 Mandatory members | `AasEnvironmentMaterializer` (V2) | OPC 10000-3 6.4.4 requires them all; without `Size` and `OpenCount` a Client cannot size a read or detect an abandoned handle. | | Round-trip oracle ignored most of the model | `AasRoundTripComparer` | `Blob`, `File`, `ReferenceElement` and `BasicEventElement` had no case arm, so their fields compared equal by falling off the switch; `valueType`, `semanticId`, `entityType` and `Key.type` were never read. The clause 6.4 losslessness claim rested on it. | Known, unchanged: calling an `Operation` with arguments answers `BadTooManyArguments`, because `NodeState.AddChild` does not route through `MethodState.FindChild`. Documented in `docs/Aas.md` §11. Not addressed in this pass, and worth a maintainer decision: OPC 30270 §9.2 says all NodeIds of Nodes not defined in the companion specification **shall not** use the standard namespaces, and Table 87 marks a vendor-specific instance namespace Mandatory. Both halves currently mint instances in the companion namespace. Separating them touches NodeId minting, the runtime and the client together, so it is better done deliberately than folded into a review pass. This is a large change and touches shared code (`Opc.Ua.Types`, the source generator, the xRegistry projection). It is deliberately a draft: the enabling changes are separable if maintainers would prefer them split into their own PRs. The latest `master` is merged into this branch in `0dd0ff0e1`. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8 Copilot-Session: c3d88950-4a65-4ff6-87d2-1f545a875c9b Copilot-Session: b8ab3763-b9bd-46ae-b0ef-045db26e5782
2937 lines
115 KiB
C#
2937 lines
115 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";
|
|
}
|
|
|
|
// The number of entries in ArrayDimensions is the number of
|
|
// dimensions, which equals the ValueRank for a fixed-size
|
|
// multi-dimensional array (e.g. "3,3" is a two-dimensional
|
|
// array with ValueRank 2).
|
|
string[] dimensions = arrayDimensions.Split([','], StringSplitOptions.RemoveEmptyEntries);
|
|
int dims = dimensions.Length;
|
|
if (dims == 0)
|
|
{
|
|
return "global::Opc.Ua.ValueRanks.OneOrMoreDimensions";
|
|
}
|
|
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)
|
|
{
|
|
// DiagnosticInfo is not a valid Variant value (OPC UA Part 6) and
|
|
// therefore has no IVariantBuilder<DiagnosticInfo>. A typed
|
|
// State<DiagnosticInfo> (scalar or ArrayOf/MatrixOf) cannot be
|
|
// emitted, so fall back to the non-generic, Variant-backed state.
|
|
// Mirrors the DiagnosticInfo exclusion in SupportsMatrixOf.
|
|
if (dataType.BasicDataType == BasicDataType.DiagnosticInfo)
|
|
{
|
|
return false;
|
|
}
|
|
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>
|
|
/// Determines whether a placeholder redeclares one its base type already
|
|
/// declares.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A placeholder is not overridden in the sense
|
|
/// <see cref="IsOverridden"/> means, because it names a slot rather than
|
|
/// a child, and merging it with the base declaration would be wrong. It
|
|
/// does however emit an Add method named after the slot, so a subtype
|
|
/// that redeclares the slot hides its base method and has to say so.
|
|
/// AASOrderedSubmodelElementCollectionType redeclaring the
|
|
/// SubmodelElement placeholder of AASSubmodelElementCollectionType is
|
|
/// the case in point.
|
|
/// </remarks>
|
|
public static bool HidesBaseTypePlaceholder(this InstanceDesign instance)
|
|
{
|
|
if (instance is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(instance));
|
|
}
|
|
if (instance.ModellingRule != ModellingRule.MandatoryPlaceholder &&
|
|
instance.ModellingRule != ModellingRule.OptionalPlaceholder)
|
|
{
|
|
return false;
|
|
}
|
|
// OveriddenNode is not linked for a placeholder, so the base chain
|
|
// is walked for a slot of the same name.
|
|
if (instance.Parent is not TypeDesign declaringType)
|
|
{
|
|
return false;
|
|
}
|
|
for (TypeDesign baseType = declaringType.BaseTypeNode;
|
|
baseType != null;
|
|
baseType = baseType.BaseTypeNode)
|
|
{
|
|
InstanceDesign[] children = baseType.Children?.Items;
|
|
if (children == null)
|
|
{
|
|
continue;
|
|
}
|
|
foreach (InstanceDesign child in children)
|
|
{
|
|
if (child.SymbolicName == instance.SymbolicName)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <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. NodeSet2-sourced
|
|
/// designs carry the verbatim UserAccessLevel bitmask when the
|
|
/// attribute is explicitly present and it is preferred; otherwise the
|
|
/// UserAccessLevel is derived from the AccessLevel, matching the
|
|
/// runtime NodeSet2 importer in <c>UANodeSetHelpers</c>.
|
|
/// </summary>
|
|
public static string GetUserAccessLevelAsCode(this VariableDesign variable)
|
|
{
|
|
uint? rawUserAccessLevel = variable?.RawUserAccessLevel;
|
|
return rawUserAccessLevel != null
|
|
? GetAccessLevelBitsAsCode(rawUserAccessLevel.Value)
|
|
: 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the WriteAccess (WriteMask) of a node onto code. The verbatim
|
|
/// OPC UA WriteMask bitmask is emitted so that attributes advertised as
|
|
/// writable in the NodeSet (for example DisplayName and Description)
|
|
/// stay writable at runtime instead of defaulting to None.
|
|
/// </summary>
|
|
public static string GetWriteMaskAsCode(this NodeDesign node)
|
|
{
|
|
uint writeMask = node?.WriteAccess ?? 0;
|
|
if (writeMask == 0)
|
|
{
|
|
return "global::Opc.Ua.AttributeWriteMask.None";
|
|
}
|
|
return CoreUtils.Format(
|
|
"(global::Opc.Ua.AttributeWriteMask){0}",
|
|
writeMask);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|