# Description Remove stock phrasing and correct misleading or inconsistent technical documentation. The initial two passes covered 367 Markdown files, followed by an interactive review of 169 editorial flags. The U5 interpretation originally deferred in that review has since received the upstream Mantis answer recorded by #4843; remaining server/spec follow-ups stay explicit in the CTT log. - Correct package responsibilities, migration steps, API names, ownership, protocol descriptions and links. Keep bundled migration guides synchronized. Preserve historical conclusions as historical records with separate re-check notes. - Provide at least two rendered TOC levels throughout `docs/`. Convert appropriate architecture figures to Mermaid without changing code examples, transcripts or historical quotations. - Address the October 19 feedback with concrete setup/migration instructions, corrected certificate paths and PubSub names, shorter repeated caveats, consistent XML comments, and a full review of the Concepts guide. Retain #4839's API corrections. - Integrate #4841's runtime/performance guidance and review incoming/recent prose. Preserve dated measurements rather than claiming new benchmark runs; clarify allocation ratios, startup-only processor settings, JIT warm-up and the benchmark commit's scope. Missing raw measurement artifacts and repeat ranges remain tracked by #4842. - Merge master through `e8c5d800a` in `32c6394b7`. Resolve the CTT log conflict using the updated Mantis states and OPC answers, retaining historical framing and re-checks for C28/C32/C43. - Address the latest naming/index feedback in `adb5bf693`: rename the guides to `docs/WotCon.md` and `docs/WotNodeSetConversion.md`; move the provider and DataType-declaration contracts into the conversion guide and remove `docs/WoT.md`. Rename `samples/DI` to `samples/Di` and `samples/Isa95` to `samples/ISA95`. Add content-index READMEs to Di, FluentApi, ISA95, Lds and PubSub; update solution entries, project references, Docker/CI paths, test fixture paths and documentation links with exact casing. **WoT guide scope.** This branch retains the detailed conversion guide that #4758 had replaced with an anchor stub. Conversion limits, verified document sets, preservation rules and provider ownership now live together in `WotNodeSetConversion.md`; binding/runtime guidance remains in `WotBindings.md` and `WotCon.md`. The design-rationale/test-ledger prose was shortened, the oversized argument-table cell moved into its section, and existing anchors preserved. **SDK policy is a separate, explicit user-directed change.** `global.json` sets a stable minimum of `10.0.400`, `rollForward: latestMajor`, and `allowPrerelease: false`. Newer major SDKs are permitted; CI's explicit SDK pins are unchanged. README, GettingStarted, DeveloperGuide and Recycling instructions reflect the possible analyzer differences. The image-contract regression fix accepts compatible stable SDK images instead of requiring exact equality with the floor. The latest four rename/index threads are resolved after publication. Three older threads remain open: universal obsolete-removal timing, unconditional nonce-strength wording, and a blanket Vision NativeAOT claim. Existing implementation/evidence constraints are explained in their inline replies. The two-level TOCs remain as requested. ## Validation - `git diff --check` and staged diff checks: passed. The untouched upstream AutoID NodeSet retains its provenance bytes, including two existing license-header trailing spaces. - `.\.azurepipelines\validate-migration-plugin.ps1`: passed; 15 bundled migration documents and manifests synchronized. - Documentation/path validation: **101** documents retain two rendered TOC levels (**2,228** entries), with idempotent formatting. **52** new relative links and their anchors passed exact-case checks. No tracked references to the old guide/sample paths remain. Both relocated provider-contract sections are preserved in the destination. - `dotnet sln UA.slnx list`: passed; shows the renamed Di/ISA95 projects and the existing FluentApi/Lds/PubSub projects. Updated solution and pump ProjectReference paths also passed exact-case existence checks. - Latest focused commands, run from the repository root: ```powershell # Passed: 61 channel send-order and reconnect tests. dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~UaSCBinaryChannelSendOrderTests|FullyQualifiedName~TcpReconnectKeyAgreementTests' --verbosity minimal # Passed: 5 pump datasheet tests; builds the renamed pump sample. dotnet test tests\Opc.Ua.Di.Tests\Opc.Ua.Di.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~PumpDatasheetConformanceTests' --verbosity minimal # Passed: 4 repository NodeSet path/round-trip tests, including the moved pump models. dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~RepositorySourceSurvivesSerializedLinkedPartitionsAsync' --verbosity minimal # Passed: build, zero warnings/errors. dotnet build samples\ISA95\MinimalIsa95Server\MinimalIsa95Server.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal # Passed: dependent sample-test project build, zero warnings/errors. dotnet build tests\Opc.Ua.WotCon.Samples.Tests\Opc.Ua.WotCon.Samples.Tests.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal ``` Earlier focused integration checks passed 2,620 tests across AutoID, event queues/scheduling, source generation, MCP/package profiles and WoT. Both reviewed Mermaid diagrams rendered locally without network access. These checks are not a full solution run. The full solution has **not** been rerun for this review. Earlier `dotnet test UA.slnx -p:CustomTestTarget=net10.0 --verbosity quiet` did not pass: three AOT projects rejected VSTest and 20 Vision tests failed during graphics initialization with `OutOfMemoryException`. Remaining long-running tests were stopped at the user's request. A focused Vision rerun passed 558 tests and reproduced those 20 failures. The earlier `dotnet test UA.slnx -p:CustomTestTarget=net48 --verbosity quiet` was blocked by the former SDK pin and was not retried after the policy change. The user approved proceeding with those limitations documented; current remote CI is not claimed green. ## Related Issues - Follow-up #4842 tracks the requested 2.0.2+ benchmark/tuning and diagnostic-tool documentation refresh. #4841 supplies documentation and earlier results, not all newly requested measurements. - #4758 explains the conversion guide's earlier consolidation; the current provider contracts are now consolidated into `WotNodeSetConversion.md`. - #4839 supplies the reviewed API corrections; #4843 supplies the updated CTT issue record. - The original prose cleanup and SDK policy were user-requested without a source issue. No new runtime architecture is introduced by these documentation edits. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] 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. - [ ] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] 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. --------- Co-authored-by: Roman Ettlinger <romanett98@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
66 KiB
WoT / NodeSet conversion
Opc.Ua.Wot.WotNodeSetConverter converts OPC UA NodeSet2 documents to
WoT Thing Models / Thing Descriptions and materializes WoT documents back
to NodeSet2. The converter preserves a byte-exact uav:nodeSet envelope
when requested, uses the structured uav:nodes projection when the
readable vocabulary is not complete, and otherwise synthesizes NodeSet2
from the readable WoT terms.
For SDK callers, ToNodeSetWithResolversAsync converts a complete
WotDocumentSet with caller-supplied thing/schema resolution while preserving
set-local precedence. It performs no transport I/O itself.
See Conversion providers for resolver ownership and DataType declaration inputs, and WoT bindings for binding authoring and runtime integration.
Contents
- Conversion providers
- Which specification revision this tracks
- Importable output
- WoT to NodeSet defaults
- Portable identity and preservation semantics
- Verified linked document sets
- Generated document-set limits
- Generated NodeIds follow Annex G.1
- Preservation digests, equality and size
- Unmapped reference vocabulary is residue
- Condition events derive from their ConditionType
- Event fields come from the type, not only from the document
- Resolving a stated selection needs the asynchronous conversion
- DataType definitions (Section 6.11)
- Model and platform vocabulary (Section 6)
- Conformance claims and strict mode (Sections 4.1, 6.1, 6.6 and 11)
- Engineering units, ranges and scaling (Sections 6.4 and 6.4.1)
- Localized text (Section 9.1.1)
- ValueRank and ArrayDimensions (Sections 7 and 9.1)
- Method arguments (Section 9.1)
- ReferenceTypes and relations (Sections 5.1.2, 5.3 and 6.2)
Conversion providers
WotNodeSetConverter converts a document to a NodeSet2 model. It resolves
declarations from the captured sibling documents of the Thing before any
loaded type. Callers supply the exact original bytes and the contextual
declarations; the converter does not perform a fresh default lookup or use an
arbitrary Web resolver.
See type binding.
An embedded uav:nodeSet needs no external model fetch. Required models absent
from both the converted document and the caller's resolver remain diagnostics.
These conversion contracts do not establish Full-profile or full JSON Schema
conformance.
DataType declaration inputs
Stored definitions can be referenced by graph identity, qualified name, or
portable NodeId. Lookup uses the carrying element's original context. Graph
identities need not resemble document locations; ambiguous owners fail.
uav:dataTypeDefinition, uav:fieldDataTypeDefinition, typed links, and
uav:dataTypeSubtypeOf contribute dependencies. Recursive fields are legal;
inheritance cycles are not. An acyclic base/derived pair in one document does
not create a cycle between documents.
Opaque metadata and literal instance values are not definition indexes.
IWotCapturedDataTypeDefinitions on the captured Thing resolver supplies
complete WotDataTypeDefinitionSource values through DataTypeDefinitions.
IWotDataTypeDefinitionResolver.ResolveDataTypeDefinitionsAsync resolves
definitions on demand. Resolvers own the documents they return; the converter
does not dispose borrowed documents and does not retain them after conversion.
Generated NodeSets declare the namespaces their Nodes own; merely referring to a namespace does not claim it.
Which specification revision this tracks
This implementation tracks WoT Binding revision 1.1, exposed by
Opc.Ua.Wot.WotBindingConformance.CurrentRevision. It determines the accepted
uav:bindingVersion values, versioned artifact base
http://opcfoundation.org/UA/WoT-Binding/v1.1/, and Section 11 name set.
The uav prefix binds to http://opcfoundation.org/UA/WoT-Binding/. The
reader matches compact uav: names without resolving the prefix's IRI, in
both permissive and strict mode. Documents using the earlier
http://opcfoundation.org/UA/WoT/v1# draft prefix binding remain readable.
Specification fixtures under tests/Opc.Ua.Types.Tests/Wot/Assets record
their source revisions and hashes in spec-examples.manifest.json.
See How this is checked for validation
and regeneration.
Importable output
A NodeSet2 document must declare names such as HasComponent or Double
in its <Aliases> table when using them as NodeIds. An undeclared name
causes BadNodeIdInvalid during import.
Both conversion directions complete missing standard aliases before returning the NodeSet. Existing declarations are retained; added aliases are sorted ordinally. Undeclared vendor aliases remain errors. Completion is idempotent: a preserved document with complete aliases needs no changes.
NodeSetComparer.CompareEquivalent resolves each document's declared aliases
first, then uses NodeSetComparisonOptions.AliasResolver if supplied.
The default comparer does not equate an undeclared name with a numeric NodeId.
WoT conversion supplies its standard-alias resolver for the completeness check.
The registry imports converted output through UANodeSet.Read and Import.
Modelling rules and the two placeholder identifiers
OPC 10000-5 assigns OptionalPlaceholder the identifier 11508 and
MandatoryPlaceholder the identifier 11510; 11509 is not a
ModellingRule Object at all. The four rules therefore map as:
uav:modellingRule |
NodeId |
|---|---|
Mandatory |
i=78 |
Optional |
i=80 |
OptionalPlaceholder |
i=11508 |
MandatoryPlaceholder |
i=11510 |
Both conversion directions derive these identifiers from one pair of constants, so a round trip preserves the modelling rule it started with.
WoT to NodeSet defaults
The table below lists the defaults the current WoT-to-NodeSet
materializer applies when the WoT input lacks information needed for a
NodeSet2 projection. Rows marked Fails emit an error diagnostic and
ToNodeSet throws. Rows marked Default substitute the listed value;
warnings or informational diagnostics are noted where the code emits
them.
| Missing WoT input | Behaviour | Materialized value |
|---|---|---|
Convertible content: no uav:nodeSet, no uav:nodes, and neither a Thing Model nor a Thing Description kind |
Fails | NoConvertibleContent error; no NodeSet is returned. |
Model namespace: root uav:id has no nsu=<NamespaceUri>;... part and document id is absent |
Default | NamespaceUris = ["urn:opcua:wot:synthesized"] and Models[0].ModelUri = "urn:opcua:wot:synthesized". This is intentionally synthetic and should be replaced by an authored namespace in portable documents. |
Root local name: root uav:browseName is absent |
Default | Sanitized title (letters, digits, _, -) when non-empty; otherwise Thing. |
Root uav:id |
Default | Deterministic NodeId by Annex G.1: ns=1;s=/nsu=<escaped model NamespaceUri>;<rootLocal>, and an informational GeneratedNodeId diagnostic. |
Root uav:browseName |
Default | 1:<rootLocal>. |
Root title |
Default | Root DisplayName becomes <rootLocal>. |
Thing Model root event annotation uav:eventType |
Default | Root is a non-abstract UAObjectType with inverse HasSubtype to BaseObjectType (i=58). If the event annotation is present, the default supertype is BaseEventType (i=2041). |
| Thing Description root type information | Default / Bound / Fails | Absent: a UAObject with HasTypeDefinition to BaseObjectType (i=58). A ua:HasTypeDefinition link or recognized compact @type name binds an existing type. Unresolved bindings fail with UnresolvedTypeBinding; conflicting, ambiguous or wrong-NodeClass bindings fail with AmbiguousTypeBinding. Other @type annotations remain residue. See type-binding resolution for context and precedence. |
Root description |
Default | No Description field is materialized. |
Property affordance uav:browseName |
Default | The affordance map key is used as the local name and BrowseName 1:<key>. |
Property affordance uav:id |
Default | Deterministic NodeId by Annex G.1: ns=1;s=/nsu=<escaped model NamespaceUri>;<rootLocal>/nsu=<escaped model NamespaceUri>;<propertyLocal>. |
Property DataSchema type or an unrecognized type |
Default | The canonical table of WoT Binding §6.11.4: boolean → Boolean, integer → the abstract Integer (i=27), number → the abstract Number (i=26), string → String, refined by contentEncoding: base64 → ByteString, format: date-time → DateTime, format: uuid → Guid, format: uri → UriString. An explicit uav:dataTypeId or uav:mapToType outranks the inference. Anything unrecognized falls back to BaseDataType (i=24). A bare integer or number is deliberately abstract: the schema states only that the value is whole or numeric, and a concrete width is recovered from an annotation rather than guessed. |
Property readOnly and writeOnly |
Default | Missing flags mean read/write access (CurrentRead | CurrentWrite, value 3). If both flags are true, the zero-access result is coerced to CurrentRead (1); this is an arbitrary safety default and should be specified explicitly. |
Property title |
Default | No DisplayName field is materialized for the variable. A titles map materializes one LocalizedText per locale, the default locale's entry first (Section 9.1.1). |
Property description |
Default | No Description field is materialized for the variable. A descriptions map materializes one LocalizedText per locale. |
Property uav:valueRank (Sections 7, 9.1) |
Default / Fails | Absent: ValueRank -1 (Scalar), which is what a NodeSet omits. Present: the stated rank, so -3, -2, -1, 0 and a fixed positive rank stay distinct. Not an integer literal, or below -3: InvalidValueRank error. |
Property uav:arrayDimensions (Sections 7, 9.1) |
Default / Fails | Absent: no ArrayDimensions attribute. Present: the ordered bounds, with 0 meaning a dimension whose length is not fixed. Not an array of non-negative integers, a length other than a fixed uav:valueRank, or any dimension against a rank that fixes none: InvalidValueRank error. |
| Property type definition | Default | HasTypeDefinition to BaseDataVariableType (i=63). An affordance that binds itself to PropertyType (i=68) is held by HasProperty rather than HasComponent, which is the only ReferenceType OPC 10000-3 reaches a Property through. |
Action affordance uav:browseName |
Default | The affordance map key is used as the local name and BrowseName 1:<key>. |
Action affordance uav:id |
Default | Deterministic NodeId by Annex G.1: ns=1;s=/nsu=<escaped model NamespaceUri>;<rootLocal>/nsu=<escaped model NamespaceUri>;<actionLocal>. |
Action title |
Default | No DisplayName field is materialized for the method. |
Action input / output (Section 9.1) |
Default / Fails | Absent: no argument Property. Present: an InputArguments / OutputArguments Property of Argument[]. Missing multi-argument order fails with MethodArgumentOrderAmbiguous; invalid schemas or ordering fail with MethodArgumentSchemaInvalid. See Method arguments for mapping and order rules. |
Event affordance uav:browseName |
Default | The affordance map key is used as the local name and BrowseName 1:<key>. |
Event affordance uav:id |
Default | Deterministic NodeId by Annex G.1: ns=1;s=/nsu=<escaped model NamespaceUri>;<rootLocal>/nsu=<escaped model NamespaceUri>;<eventLocal>. |
Event title |
Default | No DisplayName field is materialized for the event type. |
| Event type abstraction/supertype | Default | Event affordances materialize as non-abstract UAObjectType nodes with inverse HasSubtype to BaseEventType (i=2041), unless the affordance carries uav:conditionType or uav:conditionTypeId. A Condition event derives from the named ConditionType instead (WoT Binding Section 13.2). A uav:conditionType and a uav:conditionTypeId naming different types fail (ConditionTypeConflict). |
| Event source (OPC 10000-3 §7.15) | Default | A Thing Model's projected ObjectType or VariableType carries a forward GeneratesEvent reference to each event type. A Thing Description projects an Object or Variable, which cannot be the source of GeneratesEvent, so its events are declared by its type instead: an event the bound type already declares (matched by qualified BrowseName against the local context's declarations) needs nothing more, and every other one is declared by a synthesized non-abstract <rootLocal>Type (BrowseName 1:<rootLocal>Type, NodeId by Annex G.1 from that name) that derives from the bound type, or from BaseObjectType / BaseDataVariableType when the document binds none, and becomes the instance's HasTypeDefinition. Going from a NodeSet, an instance's event affordances are the events its type definition and that type's supertypes generate; a forward GeneratesEvent on the instance itself is still read. A single readable document folds a type that only carries the instance's events back into the instance - its supertype becomes the type binding - while a document set gives it a Thing Model of its own. |
Event data (Section 13.3) |
Default / Fails | Absent, or a member naming a field the projected type inherits: no Node is materialized, because BaseEventType and the ConditionTypes already declare those fields and ConditionId is the Condition's NodeId Attribute rather than a Variable. A member the type adds becomes a Property (HasTypeDefinition PropertyType, NodeId ns=1;s=/nsu=<escaped model NamespaceUri>;<rootLocal>/nsu=<escaped model NamespaceUri>;<eventLocal>/nsu=<escaped model NamespaceUri>;<fieldLocal> (Annex G.1)) whose DataType resolves by the property rules above, with uav:valueRank defaulting to -1, uav:arrayDimensions and description carried across, and HasModellingRule Mandatory when the schema lists the member in required and Optional otherwise. A member that is not a DataSchema, or that reaches a BrowseName another member already reached, fails (EventFieldInvalid) and is carried verbatim through residue. |
uav:conditionAction / uav:actsOn (Section 13.4) |
Default / Fails | Absent: the Method is a component of the Thing with no MethodDeclarationId. Present and admitted by the ConditionType the target event projects: the Method takes the base-namespace BrowseName of the named Condition Method, carries the OPC 10000-9 declaration as its MethodDeclarationId (Acknowledge i=9111, Confirm i=9113, AddComment i=9029, Enable i=9027, Disable i=9028) and becomes a component of the EventType instead of the Thing. A Method the projected ConditionType does not declare — Acknowledge or Confirm against a plain ua:ConditionType — fails (ConditionActionNotDeclared). |
Unknown event member uav:severity |
Default | WoT Binding 1.1 defines no such term. A document that carries one is consumed permissively: the member is ordinary unknown residue, nothing is materialized from it, and no Severity Property is synthesized. Strict authoring reports it as an unknown term. |
uav:modellingRule on a property or action |
Default | No HasModellingRule reference is materialized. |
uav:hasComponent / uav:componentOf entry has no matching typed ReferenceType link |
Default | HasComponent is used for the component reference. |
Link rel names a ReferenceType the local context holds (Sections 5.1.2, 5.1.5 and 6.2) |
Default / Fails | The relation is created with that exact ReferenceType, stated as a NodeSet-local NodeId. A rel matching the ReferenceType's InverseName clears IsForward; a symmetric ReferenceType reads forward under its BrowseName in both directions. A rel matching more than one ReferenceType with no uav:refId to settle it: ReferenceTypeAmbiguous error. A rel or uav:refId naming a Node of another NodeClass: ReferenceTypeNodeClassInvalid error. A rel and a uav:refId naming different ReferenceTypes, or a uav:refId naming none of an ambiguous name's candidates: ModelConceptConflict error. Nothing falls back to HasComponent or to a standard alias. |
uav:inverseName / uav:symmetric on a document that projects a ReferenceType |
Default | Absent: the ReferenceType is materialized with no InverseName and Symmetric false. Present: they become the projected Node's InverseName (tagged with the document's default locale) and Symmetric Attributes, which is what lets a local context built from documents alone resolve an inverse relation. |
Binding link has no resolvable model-name relation and no uav:refId |
Default | The link maps to Organizes. A typed link names its ReferenceType directly in rel (ua:HasComponent, ua:HasInterface, ua:NonHierarchicalReferences). |
Reference link points to another Thing by URI and no resolver is supplied or the resolver cannot find uav:id |
Default | The reference is omitted and a warning diagnostic is emitted; no placeholder NodeId is generated. |
Invalid namespace-qualified uav:id / uav:browseName syntax or unbound compact-name prefix |
Fails | An error diagnostic is emitted; ToNodeSet throws even though synthesis continues far enough to collect diagnostics. |
Event affordance says @type: uav:eventType and carries uav:isEvent |
Default | WoT Binding 1.1 defines event identity with the @type annotation and defines no uav:isEvent term. The unknown member is preserved as residue and reported as an unknown term in strict authoring. |
uav:isComposite (Section 6.1) |
Default / Fails | Absent: the type is treated as atomic and no HasComponent walk is forced. Malformed (non-boolean): InvalidModelVocabularyValue error and ToNodeSet throws. Present and valid: the flag has no distinct readable NodeSet structure, so it is carried verbatim through the uav:nodes residue and restored on the reverse conversion. |
uav:contains (Section 6.3) |
Default / Fails | Absent: sub-components come from links only. Malformed (not an array, or an entry that does not match a link uav:refName declared on the same type): InvalidContainment error. Present and valid: preserved via residue. |
uav:containedIn (Section 6.3) |
Default / Fails | Absent: no parent is recorded. Malformed (not a non-empty string, or naming the type itself, which is a cycle): InvalidContainment error. The reciprocal "the named composite exists" check is cross-document and out of scope for the single-document converter, which validates range and self-cycle only. Present and valid: preserved via residue. |
uav:unitProperty (Section 6.4) |
Default / Fails | Absent: the affordance names no unit Property. Malformed (not a canonical RFC 6901 pointer of the form /properties/<name>, naming the affordance that carries it, or resolving to something other than a sibling property affordance whose DataSchema type is string): InvalidUnitPointer error. Present and valid: the named affordance becomes the annotated Variable's own EngineeringUnits Property (HasProperty), unless it states its own uav:componentOf. |
uav:engineeringUnits (Section 6.4.1) |
Default / Fails | Absent: no EUInformation value is materialized. Malformed (not an object carrying namespaceUri, an integer unitId and displayName): InvalidEngineeringUnits error. Present and valid: the affordance's Variable gets DataType EUInformation (i=887) — unless uav:mapToType pins another — and a Value holding the EUInformation in its default XML encoding (i=888). |
minimum / maximum (Section 6.4.1) |
Default / Fails | Absent, or only one of the two: no EURange Property is materialized. minimum above maximum: InvalidRangeValue error. Present and valid: an EURange Property (Range i=884, HasTypeDefinition PropertyType, HasModellingRule Mandatory, NodeId ns=1;s=/nsu=<escaped model NamespaceUri>;<rootLocal>/nsu=<escaped model NamespaceUri>;<propertyLocal>/EURange (Annex G.1)) holding the interval, or the value of the EURange affordance the document authored itself. |
uav:instrumentRange (Section 6.4.1) |
Default / Fails | Absent: no InstrumentRange Property. Malformed, or an engineering range not contained in it: InvalidRangeValue error. Present and valid: an InstrumentRange Property (Range i=884, HasModellingRule Optional) holding the interval. |
uav:scaleFactor (Section 6.4) |
Default / Fails | Absent: identity scaling (factor 1). Malformed (not a non-zero number): InvalidModelVocabularyValue error. Present and valid: preserved via residue. It is a static presentation and transport transform, never derived from — nor used to derive — EngineeringUnits, EURange or InstrumentRange. |
uav:decimalPlaces (Section 6.4) |
Default / Fails | Absent: no rounding is recorded. Malformed (not an integer greater than or equal to zero; 2.0 is rejected as a non-integer literal): InvalidModelVocabularyValue error. Present and valid: preserved via residue. |
titles / descriptions (Section 9.1.1) |
Default / Fails | Absent: the singular member materializes one LocalizedText using its effective language. Present: every plural entry is authoritative. The singular member repeats the default-locale entry when present; otherwise it repeats the code-point-first language entry and uses a node-local language-neutral override. A missing default locale is valid. A missing singular member, an inconsistent fallback, or an incorrectly language-tagged fallback produces InvalidLocalizedText. |
uav:semanticId (Section 6.7) |
Default / Fails | Absent: no semantic reference is recorded. Malformed (not an absolute IRI with a scheme): NonAbsoluteIri error. Present and valid: preserved via residue. |
uav:metadata (Section 6.7) |
Default | Absent: nothing is recorded. Present: opaque; carried verbatim through residue, never validated and never a reason to reject the document (Section 6.7). |
uav:propertyConfiguration (Section 6.7) |
Default | Absent: nothing is recorded. Present: opaque per-affordance configuration; carried verbatim through residue and never validated. |
uav:actionConfiguration (Section 6.7) |
Default | Absent: nothing is recorded. Present: opaque per-affordance configuration; carried verbatim through residue and never validated. |
uav:eventConfiguration (Section 6.7) |
Default | Absent: nothing is recorded. Present: opaque per-affordance configuration; carried verbatim through residue and never validated. |
uav:includeInherited (Section 6.8) |
Default / Fails | Absent: no inheritance-span flag is recorded. Malformed (non-boolean): InvalidModelVocabularyValue error. Present and valid: preserved via residue. |
uav:additionalProperties (Section 6.8) |
Default / Fails | Absent: no open-content flag is recorded. Malformed (non-boolean): InvalidModelVocabularyValue error. Present and valid: preserved via residue. |
uav:browsePathAnchor (Section 5.1.4) |
Default / Fails | Absent: a relative uav:browsePath resolves against the nearest enclosing uav:id. Malformed (not an ExpandedNodeId): ValidationError error; the session-local ns=<index> form is reported NonPortableIdentity (an error unless AllowNonPortableIdentifiers is set). Present and valid: preserved via residue. |
Portable identity and preservation semantics
The following sections describe generated NodeId identity and the distinct measurements used for preserved JSON values.
Verified linked document sets
WotNodeSetConverter.FromNodeSetDocumentsAsync exports a linked set and
reconstructs it before returning success. Each document root owns a disjoint
partition of the source. Methods, their argument Properties, and nested
Variables stay with their owner; nested Objects and EventTypes have separate
document roots. References can point at Nodes in other partitions.
Complete readable partitions omit uav:nodes. A partition requiring exceptional
preservation carries its complete authoritative uav:nodes representation,
not a delta to overlay on readable siblings. The namespace/model/alias tables,
values, permissions, and extensions remain part of the fidelity check. Duplicate
native ownership, conflicting shared headers, and attempts to add readable Nodes
to another native partition are rejected.
WotNodeSetConverter.CompareDocumentSet compares the reconstructed set with
its source, normalizing only the order of top-level Node records that partitioning
can regroup. Reference direction and order, definition-field and argument order,
attributes, values, and metadata remain compared. The synchronous
FromNodeSetDocuments API produces the readable candidate; use the asynchronous
API when verified reconstruction is required.
MergeNodeSetPartitions applies the same bounded ownership and header rules
to already converted partitions without modifying its inputs. The registry
uses this when preparing one runtime source from several documents that declare
the same model. Shared model ownership joins their activation closure; unrelated
readable documents do not become one atomic closure merely because their NodeIds
use the same namespace.
Generated Double constants, ranges, and sampling intervals use the shortest round-trip spelling. Received numeric literals retain their validation rules.
WotConversionResult<WotDocumentSet> exported =
await WotNodeSetConverter.FromNodeSetDocumentsAsync(
nodeSet, "pump", nodeResolver: resolver, cancellationToken: ct);
if (!exported.Success)
{
throw new InvalidOperationException(string.Join(Environment.NewLine, exported.Diagnostics));
}
using WotDocumentSet documents = exported.Value!;
foreach (WotDocumentSetEntry entry in documents.Entries)
{
Console.WriteLine($"{entry.Href}: {entry.Document.Utf8Json.Length} bytes");
}
Opaque preservation is not limited to uav:nodes. Extension members and WoT
metadata without a native NodeSet mapping are retained as JSON residue while
the converter processes the document's supported readable members. For example,
an unsupported future uav:nodes grammar is not parsed: the converter reports
the unsupported profile and retains the complete object as opaque residue
instead of discarding it or guessing how to interpret its records. This does
not bypass validation of supported content; malformed supported grammars and
invalid archival digests remain errors.
When an archival uav:nodeSet is present, known readable facts are checked
against that baseline and conflicts are reported rather than overwritten.
Routing-only enrichment does not grant permission to add or replace archived
model facts.
Generated document-set limits
Converter callers can set MaxDocumentSetDocuments, MaxDocumentSetDepth and
MaxDocumentSetBytes independently of external resolver limits. Generated
document count, containment depth and cumulative JSON bytes are checked during
conversion, including preservation partitions. Defaults impose no additional
set-wide ceiling, preserving existing large companion-model conversions.
Hosts processing untrusted input should set all three limits.
Generated NodeIds follow Annex G.1
A document that authors no uav:id gets a generated NodeId, and the
generation is the Annex G.1 formula rather than a convenience of this
implementation:
GeneratedNodeId(U, P) = "nsu=" + U + ";s=" + P
U is the NamespaceUri the synthesized Node is created in, and P is the
Node's absolute browse path in OPC 10000-4 Annex A.2 relative-path syntax: each
element is preceded by /, an element of the base OPC UA namespace is written
bare, any other element is nsu=<percent-encoded NamespaceUri>;<name>, and the
Annex A.2 reserved characters &/.<>:#! are escaped with & inside a name. A
NodeSet file carries the same identity in its NodeSet-local spelling,
ns=1;s=<P>, because namespace index 1 is U; the reverse mapping renders it
back as nsu=U;s=P.
WotPortableIdentity.GenerateNodeId / GenerateBrowsePath is the single
implementation, so a conversion and a published Annex G.1 vector measure the
same function. The same class answers IsPortableNodeId,
IsPortableQualifiedName and IsResolvableBrowsePath, which the
conversion's Section 5.1.1 and 5.1.4 validation calls.
The leading separator, per-element namespace qualification and escaping make
the generated identity injective. A member named A/B of Root has a different
identifier from a member named B of Root/A, and a base-namespace
InputArguments has a different identity from a model member with the same
name. A document-authored uav:id always wins over generation; author one when
the identity must be fixed independently of its browse path.
Preservation digests, equality and size
Annex G distinguishes retained-byte digests, JSON-value equality and compact-received-form size. This implementation keeps those operations separate.
- Retained-byte digests. Residue digests hash the decoded bytes without
canonicalization or reformatting. The
sha256of auav:nodeSetenvelope anduav:sourceDigestof a projection source also hash retained bytes. A mismatch is an integrity error. - JSON-value equality.
WotDocument.ToCanonicalUtf8writes the RFC 8785 (JCS) form used by the Section 9.4 residue-conflict comparison. Object member order, equivalent escapes, and1.0versus1do not create conflicts. Numbers that cannot be represented without precision loss, such as9007199254740993, produce diagnostics; comparison then uses the written forms. Preservation digests do not hash this canonical form. - Compact-received size. Section 6.6's opaque-object limit measures the
received JSON after removing only RFC 8259 whitespace outside strings.
Member order, number spelling and string escapes remain unchanged.
For example,
1.0still counts as three UTF-8 bytes, not one. Depth and key-count limits apply to the parsed value.
Unmapped reference vocabulary is residue
The Binding vocabulary does not define uav:capability,
uav:componentModel, uav:reference, uav:congruentType,
uav:congruentTypeName or uav:nameNamespace. The converter treats them as
unmapped JSON and carries them through the Section 9 uav:nodes residue
mechanism without creating References or emitting a permissive-mode diagnostic.
Use the following Binding forms to state those relationships:
| Unmapped term | Binding form |
|---|---|
uav:componentModel |
link rel names ua:HasComponent directly |
uav:capability |
link rel names ua:HasInterface directly |
uav:reference |
link rel names ua:NonHierarchicalReferences directly |
uav:congruentType + uav:congruentTypeName |
Section 5.2.1 type binding with @type and a ua:HasTypeDefinition link |
See Resolving a type binding: the local context for the Section 5.2.1 binding rules.
Condition events derive from their ConditionType
A projected event normally derives from BaseEventType (i=2041). If the event
affordance carries uav:conditionType or uav:conditionTypeId, Section 13.2
instead makes the event type derive from the named ConditionType. The Condition
mapping is summarized in Alarms and Conditions.
This changes event-filter behaviour. A client filtering for BaseEventType
subtypes still matches the projected Condition event, but a client that assumed
the exact immediate supertype was BaseEventType, or that selects
Condition-specific fields, observes a different type hierarchy and field set.
Event fields come from the type, not only from the document
The fields a notification carries are declared by BaseEventType and, for a
Condition, by the ConditionTypes of OPC 10000-9 — types a converted NodeSet
almost never contains. Both directions therefore read one table of those
declarations rather than the Nodes:
- going from a NodeSet, an event affordance's
dataobject states the effective field list of the projected type — the inherited fields in inheritance order, then the Variables the type declares itself in the order its References state them — so a document describes the notification a consumer actually receives rather than an empty schema; - going to a NodeSet, only the members the projected type adds become Nodes, because re-declaring an inherited field would leave a Server holding two declarations of one field.
A NodeSet holding an Object and the EventType it raises is projected about the Object: an EventType something else generates is a declaration that Node uses, not the subject of the document.
The data object states what a notification carries; the affordance's tm:ref
and uav:eventSelectClauses state what a MonitoredItem asks for (Section 6.1).
The schema is derived from the type; the request is a link to the EventType
definition the fields come from, refined by the clauses the affordance writes,
and where the affordance states neither a consumer selects the eight mandatory
BaseEventType fields.
Going from a NodeSet, an EventType Node becomes a Thing Model of its own
whose root carries uav:eventType, the portable uav:id, and the effective
data schema with a uav:fieldOrder on every walked object of more than one
property — which is exactly the EventType definition a fast path derives its
baseline from. Where a document set carries that sibling document, the Object
or ObjectType that raises the event names it with tm:ref; a document that
projects the EventType itself keeps its inline definition and does not reference
itself.
Going to a NodeSet, the link is resolved before the EventType's fields and
Condition Methods are synthesized, so an affordance that declares no data of
its own materializes the field set the linked definition declares. The clause
list itself projects to no Node — it is a client-side request rather than a
model fact — and the OPC UA binding runtime compiles the resolved selection into
the EventFilter.SelectClauses of the MonitoredItem (see
WotBindings.md).
Resolving a stated selection needs the asynchronous conversion
An EventType definition is a document, so deriving a selection means following
a document link. Only WotNodeSetConverter.ToNodeSetResultAsync does that, and
only when it is given an IWotThingResolver holding the local document context
of Section 5.1.5:
WotConversionResult<UANodeSet> result = await WotNodeSetConverter
.ToNodeSetResultAsync(document, options, thingResolver, cancellationToken: ct)
.ConfigureAwait(false);
The synchronous ToNodeSetResult and ToNodeSet follow no link. An affordance
that states a selection — a tm:ref, uav:eventSelectClauses, or both — is
therefore reported with WotDiagnosticCode.EventSelectionUnresolved rather than
converted, because an EventType materialized without the fields its linked
definition declares is indistinguishable from one that never had any. Where
every reference a document writes is local to that document,
NullWotResolver.Instance is the explicit "no external resolution" policy that
still takes the asynchronous path. An affordance that states no selection needs
no resolution: it takes the implicit BaseEventType default and converts
synchronously as before.
DataType definitions (Section 6.11)
WoT Binding §6.11 gives Structures, Unions, Enumerations, OptionSets and
SimpleDataTypes a readable vocabulary, so that defining one is no longer on
its own a reason to fall back to the native uav:nodes projection. §6.11.8
states that as a completeness contract: a fact the clause covers shall be
emitted readably and shall not be the reason a converter adds uav:nodes.
Both directions are implemented.
Authoring. uav:dataTypeDefinitions on the Thing root holds the
definitions. Each is a JSON-LD node with an @id, a @type of
uav:StructureDefinition, uav:EnumDefinition or uav:SimpleDataType, and a
uav:dataTypeName. The complete definition occurs in exactly one place and
every other occurrence is an @id-only reference; two occurrences that each
contribute properties are rejected rather than merged, because merging two
ordered field lists has no defined answer.
Identity. uav:dataTypeId states the NodeId where the author wants to.
Otherwise it is derived from uav:dataTypeName alone as
nsu=<NamespaceUri>;s=DataTypes/<Name>, so the same definition read from a
differently ordered or differently nested document still lands on the same
Node. A field may name a sibling definition by its JSON-LD @id; that @id
is a graph identifier and is resolved to the sibling's NodeId, never read as
a NodeId itself.
Inference. A DataSchema with no explicit definition infers one, but only where it determines every required fact — inference fails rather than guesses:
| Schema | Result |
|---|---|
type: object with one property |
A Structure. |
type: object with more than one property |
Requires uav:fieldOrder; JSON member order carries no meaning, so without it the encoding order is unknowable and inference fails. |
required |
Decides IsOptional; all required gives Structure, otherwise StructureWithOptionalFields. |
uav:structureType: Union |
A Union. |
type: integer with oneOf branches carrying const and uav:enumName |
An Enumeration. |
type: integer with a bare enum array |
Not an Enumeration: it states values but never names them. |
A bare integer or number inside a Structure field |
Fails. It is honest about a scalar Variable, where the abstract type permits subtype values, but inside a Structure accepting them would need a subtyped-value kind the schema has not asked for. |
A custom type named without uav:dataTypeSubtypeOf |
Fails. §6.11.4 forbids a custom type subtyping the abstract Integer or Number. |
Encodings. A non-abstract Structure or Union exposes Default Binary,
Default XML and Default JSON, with identities derived by appending
/Default Binary and so on to the type's own identity. An abstract type is
refused encoding identities outright. A concrete type used only inside other
Structures — never directly in an ExtensionObject — may set
uav:hasDefaultEncoding: false, and then no encodings are generated for it;
the term is refused on any kind that has no encodings to begin with.
An OptionSet states a base of Byte, UInt16, UInt32 or UInt64 wide
enough for its highest authored bit. The abstract UInteger is not legal: the
base has to say how many bits exist, and an abstract type says only that there
are some.
NodeSet to WoT. Every DataType a NodeSet defines is emitted back into
uav:dataTypeDefinitions. Whether a definition is an enumeration is decided
by the shape a NodeSet actually gives it — an enumeration field carries a
value and no DataType, a structure field the reverse — because the file states
no kind directly. The same is true of the structure kind: only IsUnion is
recorded, so optionality and subtype allowance are read back off the fields.
An encoding link is searched from both ends, because a NodeSet may write it
from either and real companion models write it from the Object. An alias is
resolved rather than emitted, since a name like DataType="Structure" means
nothing outside the document that defines it.
Known gap. An inferred definition's own DataSchema terms
(uav:fieldOrder, properties, required, oneOf) still travel as residue
rather than being re-derived from the definition, so a document that relies on
inference does not yet reproduce byte-identically and keeps its uav:nodes
projection. Closing it is the canonical-schema equivalence of §6.11.6:
derive the schema from the definition, normalize both, and require the two
semantic normal forms to be equal.
Model and platform vocabulary (Section 6)
The WoT Binding Section 6 model- and platform-vocabulary terms
(composition, containment, naming, semantics, inheritance) and the
anchored browse-path term of Section 5.1.4 are readable annotations:
they record OPC UA model facts but have no distinct structure that this
converter materializes into the readable NodeSet (it does not model, for
example, a HasDictionaryEntry structure). The converter therefore
handles them in one direction with full round-trip fidelity:
- WoT to NodeSet. Each term is validated during synthesis against
the per-term domain and range table of Section 7. A malformed value is
an error (Section 7 requires a consumer to treat the document as
invalid rather than repair it), so
ToNodeSetthrows andToNodeSetResultreports the diagnostic. A well-formed value is carried unchanged through the genericuav:nodesresidue mechanism. - NodeSet to WoT (reverse). These readable terms are not synthesized
from a plain NodeSet. When a NodeSet carries a structure that the
readable vocabulary cannot yet express, the complete
uav:nodesnative projection preserves it losslessly, and any residue previously captured for a term is re-applied by JSON Pointer. - Round-trip. A document carrying these terms survives
WoT → NodeSet → WoT unchanged. Affordance-level terms
(
uav:scaleFactor,uav:decimalPlaces,uav:semanticId) are preserved under the affordance's projected local name, so an affordance that also carriesuav:browseNameround-trips under that browse name's local part rather than its original map key.
The opaque terms uav:metadata, uav:propertyConfiguration,
uav:actionConfiguration and uav:eventConfiguration are never read and
never cause rejection; they are carried verbatim. Their shape is
checked, because a consumer that must carry a value unchanged and must
not reject it is otherwise obliged to carry an unbounded, unattributable
value (Section 6.6): every top-level key is an absolute IRI or a compact
IRI whose prefix the document's @context binds, and the object stays
within 65 536 octets in the compact received form of Annex G.4 (see
Preservation digests, equality and size),
32 levels of nesting and 256 top-level keys. Revision 1.0 stated no key
rule, so a document whose keys are not namespaced is preserved and
reported as deprecated rather than rejected; strict conformance (below)
turns the same finding into an error.
Conformance claims and strict mode (Sections 4.1, 6.1, 6.6 and 11)
The following terms carry document-level metadata or annotate event affordances and security schemes. They do not become Nodes themselves and survive a round trip verbatim through the residue mechanism:
| Term | Where | What is checked |
|---|---|---|
uav:bindingVersion |
TD/TM root | a <major>.<minor> revision string (Section 4.1) |
uav:profile |
TD/TM root | a non-empty array of WoT-<name> conformance claims |
uav:eventSelectClauses |
event affordance | a non-empty ordered list of clauses carrying exactly tm:ref and a relative uav:browsePath, with no two clauses of the final selection materializing the same data member path (Section 6.1) |
tm:ref |
event affordance and select clause | a document URI with an optional RFC 6901 JSON Pointer resolving, in the local document set, to an EventType definition that carries uav:eventType, a portable uav:id, an object data schema and no selection of its own (Section 6.1) |
uav:minimumSecurity |
auto security scheme |
uav:securityMode, uav:securityPolicy, or both, and no other member (Section 5.7.1) |
Authoring a claim and processing one are different acts
Section 4.1 states the two checks side by side and makes them deliberately
different, and WotNodeSetConverterOptions.AuthoringValidation selects
between them:
| Consumer (the default) | AuthoringValidation = true |
|
|---|---|---|
uav:bindingVersion |
shall be <major>.<minor>; a well-formed value this library does not implement is a UnsupportedBindingRevision warning and is preserved |
shall name a revision this Binding publishes (1.0 or 1.1); anything else is an error |
uav:profile |
shall be a non-empty array of WoT-<name> strings; an entry Section 11 does not define is an UnrecognizedConformanceClaim warning and is preserved |
shall name only units and profiles Section 11 defines |
unit carrying a quantity kind |
a QuantityKindInUnit warning; the authored value is preserved and never reinterpreted |
an error (Section 6.4) |
A consumer that rejected a document for naming a revision or a unit it does not know would be refusing to read a document that is syntactically valid, whose known terms it understands, and whose unknown terms it is already required to carry — which is the failure that makes a vocabulary unextendable. A consumer enforces the syntactic rule in every mode, so a malformed claim remains an error.
Both published revisions are read here. WotBindingConformance.SupportedRevisions
names 1.0 and 1.1, and the two places revision 1.0 differs — unnamespaced
opaque keys (Section 6.6) and a quantity kind in unit (Section 6.4) — are
reported as deprecated and preserved rather than rejected.
Opc.Ua.Wot.WotUnitMigration.MoveQuantityKinds is the opt-in helper that moves
such a value to qudt:hasQuantityKind; it never invents an engineering unit in
the vacated member, because none is recoverable from a quantity kind, and it
leaves a value alone where the affordance already states a different quantity
kind.
WotNodeSetConverterOptions.ConformanceMode chooses how strictly the rest
is held:
Permissive(the default) processes what it understands and preserves the rest. An unknownuav:term is carried unchanged as residue rather than reported, and an unnamespaced opaque key is a warning. This is what Sections 4.1, 6.6, 9.4 and 10.2 require of a consumer.Strictadditionally reports auav:term this revision does not define, an opaque object that breaks the key or bound rules, and — whereRequiredConformancenames units or profiles — a claim the document does not make. Claiming a profile claims every unit it names, soWoT-Modellersatisfies a requirement ofWoT-EventMapping.
Strict mode is for authoring and conformance testing: a misspelled term should fail there rather than travel silently. It is never the default, because a consumer is not allowed to reject a document for carrying a term it does not know.
The revision this library implements, the Section 11 name set, the
opaque bounds and the security strength orders are stated once, in
Opc.Ua.Wot.WotBindingConformance; the select-clause term, its shape rules,
its member-naming rule and its documented BaseEventType default are stated
once, in Opc.Ua.Wot.WotEventSelectClauses. WotBindingConformance.VocabularyTerms
contains the revision 1.1 IRIs and the additional Call, projection, declaration,
encoding and event-identity terms implemented by this library. IsKnownTerm
recognizes prefixed document terms; ScopedTerms / IsScopedTerm recognize
the vocabulary IRIs introduced by scoped contexts, such as uav:unitNamespaceUri,
uav:unitId, uav:rangeLow and uav:sha256. Pass the compact uav: IRI to
IsScopedTerm, not the JSON object's short member name. Use these APIs rather
than a fixed term count.
The uav:nodes record grammar is not vocabulary
The member names inside uav:nodes — nodeClass, nodeId, browseName,
references and the rest — are the lower-camel-case spellings of the UANodeSet
XSD field names. They expand in the record-grammar namespace
WotBindingConformance.NodesVocabularyNamespace
(http://opcfoundation.org/UA/WoT-Binding/nodes/) rather than in the term
namespace, because they are a versioned record format and not vocabulary that
outlives it. Keeping them apart is also what stops a grammar token from
colliding with a class annotation of the same spelling: a node record's
dataType member holds the ExpandedNodeId of a Variable's DataType and is not
the NodeClass annotation uav:dataType, and a reference record's
referenceType member is not uav:referenceType. Strict conformance skips both
structured projections whole, so a record member name is never reported as an
unknown term.
Generated documents state the revision they were generated against
Section 4.1 requires a generator to state uav:bindingVersion, because a
generator — unlike a hand author — always knows which revision it emitted. Every
document FromNodeSet produces therefore carries
uav:bindingVersion: "1.1". An authored claim wins over that stamp: a document
that declared 1.9 round-trips as 1.9, because a claim a consumer shall
preserve is not one it may overwrite. A claim that agrees with the stamp is
re-derived rather than also carried as residue.
NodeClass and ReferenceType vocabulary
The NodeClass annotations and ReferenceType Attributes are:
| Term | Where | What it says |
|---|---|---|
uav:referenceType |
@type at TM root |
the document projects a ReferenceType Node (Sections 5.2 and 6.2.1) |
uav:dataType |
@type at TM root |
the document projects a DataType Node, whose definition travels in uav:dataTypeDefinitions |
uav:inverseName |
TM root with uav:referenceType |
the OPC 10000-3 InverseName, in the document's default locale |
uav:symmetric |
TM root with uav:referenceType |
the OPC 10000-3 Symmetric flag, false where absent |
A @type carries at most one NodeClass annotation — a Node has exactly one
NodeClass — and a second is a NodeClassAnnotationConflict error. A document
that sets uav:symmetric: true shall not carry uav:inverseName, because a
symmetric Reference reads the same in both directions; that, and either term on
a document that projects no ReferenceType, is a
ReferenceTypeProjectionInvalid error.
WoT Binding 1.1 defines neither uav:severity nor uav:isEvent. Event identity
comes from the @type: uav:eventType annotation alone, and Severity is a field
of an occurrence carried by the notification data schema rather than affordance
metadata. Generated documents emit neither member, nothing is synthesized from
either, and permissive conversion preserves either unknown member as ordinary
residue. Strict authoring reports either as an unknown term.
Engineering units, ranges and scaling (Sections 6.4 and 6.4.1)
unit, uav:unitProperty, uav:engineeringUnits, minimum/maximum and
uav:instrumentRange are mapped, not carried: each names a Property Node
of an AnalogUnitType or AnalogItemType Variable, and both directions
materialize it.
Five of the uav: terms — uav:engineeringUnits, uav:unitProperty,
uav:instrumentRange, uav:scaleFactor and uav:decimalPlaces — describe the
Variable a property affordance projects, and are rejected anywhere else. The
role is decided once, at the document root: the members of the root properties
map are property affordances, and everything below one — a member of an action's
input, of an event's data, or of a property affordance's own DataSchema —
rejects all five. A field of a payload is not a Variable, so it carries no
EngineeringUnits Property for a unit to describe, and a properties map deeper
in a document is a DataSchema member map rather than an affordance map however it
is spelled.
NodeSet to WoT. For every Variable that holds them, the converter decodes the three OPC 10000-8 Properties:
EngineeringUnitsprojects to a property affordance of its own. That affordance carriestype: "string"— what a client reads there at run time — the definitiveuav:mapToTypei=887, anduav:engineeringUnitswith theEUInformation'snamespaceUri, integerunitId,displayNameanddescription. A display string alone cannot recover the authority or its machine-readable code.- The annotated Variable's affordance states
unit(theEUInformationDisplayName, never a quantity kind) anduav:unitProperty, a canonical/properties/<name>pointer at that sibling affordance. EURangebecomesminimumandmaximum;InstrumentRangebecomesuav:instrumentRange.
Only a Property whose value actually decodes contributes. A foreign encoding, a
partial structure or a missing value leaves the Property an ordinary affordance,
the readable mapping is then incomplete, and the uav:nodes projection carries
it — a reported gap rather than a number nobody wrote. Nothing is ever derived
from the width of a DataType: an Int16 reads from −32768 to 32767, and that is
a fact about the machine representation rather than an engineering range.
WoT to NodeSet. uav:engineeringUnits materializes the EUInformation
value; uav:unitProperty makes the affordance it names a HasProperty child of
the annotated Variable, unless that affordance states its own uav:componentOf;
minimum/maximum and uav:instrumentRange materialize EURange and
InstrumentRange Properties (Range i=884, PropertyType), or fill in the
value of the Node an authored EURange/InstrumentRange affordance already
produced.
uav:scaleFactor and uav:decimalPlaces stay readable annotations carried
through residue. Section 6.4 describes them as a static presentation and
transport transform, so a converter neither derives them from the analog
Properties nor derives those Properties from them; a source NodeSet that states
neither never gains either.
Localized text (Section 9.1.1)
DisplayName and Description are LocalizedText, and both directions carry
every locale.
NodeSet to WoT. The document's default locale is the locale the root Node's
own DisplayName (or Description) states, declared as the @language of the
generated @context; a source that names none declares none, and Section
9.1.1's en then applies. A Node with one locale writes title and
description alone. A Node with several writes the plural titles and
descriptions maps as well. The plural member is authoritative: it carries
every locale the source had, and the singular member is the default projection a
consumer that knows nothing of the plural members reads. Where the plural member
has an entry for the document's default locale the singular member is that
entry, so the two never disagree. Where it has no such entry — a Node
authored in the plant's language and read against another default locale — the
plural member is still written in full and the singular member carries the entry
whose BCP 47 tag is first in ascending Unicode code-point order (Annex G.3), as
a display fallback that asserts no locale. The document does not claim that text
is written in the default locale, and nothing is pushed into the exceptional
uav:nodes projection to say something the plural member already says.
Asserting no locale is a claim a JSON-LD reader has to be told about. title and
description are terms of the W3C Thing Description context, and a @context
that declares @language tags every unqualified value with it - so a German
singular member would expand as English text. Where any projected text states no
entry for the document's default locale, the generated @context therefore
carries one further entry re-declaring the two terms with "@language": null.
It is written only where the document needs it: adding it unconditionally would
strip the language tag from every document this library writes. Being derived
from the projected Nodes it is re-derivable and is not also captured as residue;
an author's own override of the same terms says something different and is kept.
The same problem has a different answer inside uav:engineeringUnits. Section
6.4.1 mints displayName and description there as short members under a
type-scoped context, so a root-level override cannot reach them: the scoped
context is entered on that object and nowhere else. Where the EUInformation's
text is not in the document's default locale, the object therefore carries its
own node-local @context re-declaring displayName as uav:unitDisplayName
and description as uav:unitDescription, each with "@language": null.
namespaceUri and unitId are short members of that same scoped context, which
is why the generated document names the Binding context itself
(http://opcfoundation.org/UA/WoT-Binding/v1.1/opc-ua-wot-binding.context.jsonld)
alongside the W3C one: a short member is a term only while the context defining
it is in scope, and a document that named only the uav prefix would expand
those members to nothing. The identity is version-pinned because a document
states which revision it was written against, and a context that moves under a
document is a document whose meaning changed without it being edited.
WoT to NodeSet. A plural member becomes one LocalizedText per entry. The
entry written first — the one the Node's own attribute carries — is the
default-locale entry where the map has one and the code-point-first entry
otherwise, which is the same entry the singular member carries, so the round
trip is stable. A singular member alone becomes one LocalizedText tagged with
the document's declared @language, or untagged where the context declares none
— the form a UANodeSet writes when it names one language without saying which.
The same selection is used wherever a term reduces a LocalizedText to one
string: a ReferenceType's uav:inverseName, and the displayName and
description of uav:engineeringUnits. The unit member of the annotated
Variable is taken in that same locale, so a multi-locale EUInformation states
one text in both places instead of falling back to preservation because the two
disagree.
The mapping applies to the root, to property, action and event affordances, to
event fields, to Method argument descriptions, and to DataType definitions and
their structure and enumeration fields. A field's DisplayName maps to title.
ValueRank and ArrayDimensions (Sections 7 and 9.1)
A DataSchema's type says whether a value is an array, not which of the five
things an OPC 10000-3 ValueRank says. uav:valueRank and uav:arrayDimensions
are therefore emitted and read on ordinary Variable affordances as well as on
Method arguments, event fields and DataType-definition fields.
The scalar rank -1 is the default a NodeSet omits, so it is not restated;
-3 (ScalarOrOneDimension), -2 (Any), 0 (OneOrMoreDimensions) and every
fixed positive rank are written and read back exactly, and none of them is
collapsed to a scalar. uav:arrayDimensions carries one bound per dimension —
0 for a dimension whose length is not fixed — so its length is the rank by
construction: a count that disagrees with a fixed rank, or any dimension against
a rank that fixes none, is an InvalidValueRank error rather than a silently
malformed Variable.
Method arguments (Section 9.1)
A UA Method's InputArguments and OutputArguments are the WoT action's
input and output DataSchemas, in both directions.
NodeSet to WoT. The Argument structures the argument Properties hold are
decoded into an object DataSchema whose members are the arguments, whose
uav:fieldOrder states their declaration order — the order an OPC 10000-4
Call is positional over — and whose required lists all of them, because a
Call supplies all of them. Each member carries the WoT type members that stand for its DataType, the
definitive uav:mapToType, uav:valueRank, any uav:arrayDimensions and its
Description. An argument Property represented by the schemas is not also
emitted as a sibling property of the Thing. A Property whose value cannot be
decoded remains a sibling so no Node is lost. The readable schemas state what
the arguments are and not which Nodes hold them, so the exact NodeId and
attributes of the argument Properties travel in the uav:nodes preservation
projection.
WoT to NodeSet. Each supplied schema creates an InputArguments or
OutputArguments Property (Argument[], DataType i=296, ValueRank 1)
held by HasProperty. Its generated NodeId appends the bare base-namespace
InputArguments or OutputArguments name to the action's Annex G.1 path.
A schema naming one DataType through uav:mapToType, uav:dataTypeId,
uav:dataTypeName or inline uav:dataTypeDefinition creates one argument.
Its name comes from uav:browseName, then title, then Input or Output.
Otherwise, the object schema's members are the arguments. Their DataTypes use
the property mapping rules; uav:valueRank defaults to -1, and
uav:arrayDimensions and description are copied to each Argument.
uav:fieldOrder sets argument order. A single argument needs no order annotation;
Acknowledge, Confirm and AddComment use the OPC 10000-9 order EventId,
Comment. Other multi-member schemas without ordering fail with
MethodArgumentOrderAmbiguous. An order that does not list every member exactly
once, or a schema that is not a JSON object, fails with
MethodArgumentSchemaInvalid. The authored schema remains in residue for
diagnostic and preservation purposes.
ReferenceTypes and relations (Sections 5.1.2, 5.3 and 6.2)
The readable mapping carries some References structurally — containment as
affordances and uav:hasComponent / uav:componentOf, the type hierarchy as
tm:extends, the type definition as a ua:HasTypeDefinition link, the
modelling rule as uav:modellingRule, the event source as an event affordance
and a DataType's encodings as uav:defaultEncodingId. Section 6.2 says a
Reference is a single relation and a document shall not be read as declaring
two, so none of those is written a second time.
NodeSet to WoT. Every other Reference — a companion model's own
ReferenceType, ua:HasInterface, ua:Organizes — is written as a typed link:
the ReferenceType's compact model name in rel, its portable
ExpandedNodeId in uav:refId, and the target's own BrowseName in
uav:refName where the NodeSet holds the target. A reference that runs inverse
is written under the ReferenceType's InverseName, which states the
direction. The compact name comes from the NodeSet itself, in order of
specificity: its own UAReferenceType declarations (the only place an
InverseName and the Symmetric flag are stated), its <Aliases> table, then the
standard base-namespace names. A ReferenceType none of the three names, and the
inverse direction of one that declares no InverseName, are reported
(ModelConceptUnresolved) rather than written under a substitute relation: a
link whose rel named a different ReferenceType, or whose direction was
silently reversed, would read as a fact the source never stated.
A document that projects a ReferenceType Node itself additionally carries
uav:inverseName and uav:symmetric, so a set of documents describes a
companion model's relations completely enough to convert back.
WoT to NodeSet. See the ReferenceType rows of the defaults table above, and
Resolving a relation: companion ReferenceTypes
for how a rel and a uav:refId are resolved against the Section 5.1.5 local
context.
uav:componentOf and its ua:ComponentOf alias
Section 9.1 spells the parent-placement relation uav:componentOf and declares ua:ComponentOf as an alias of it. Both
spellings are accepted wherever the relation is read, and they are treated as
the same term rather than as a term and a lookalike: the alias reads as a
compact model name whose local part is the InverseName of HasComponent, so it
is intercepted as a binding term instead of being realized a second time as a
generic inverse typed link.
This implementation writes uav:componentOf and accepts but does not emit the
alias, keeping one relation in one spelling on output.
Projection documents and the OPC 10100-1 v1.02 asset surface
Two neighbouring subjects are documented elsewhere so they are stated once:
- A projection document declares affordances instead of defining them, and
Section 12.5 closes the set of members it may annotate beside
tm:ref. See Projection documents and the View NodeClass. - The OPC 10100-1 v1.02 asset surface (
CreateAsset,WoTFileupload, theAssetRegistryPOCO reader) is a separate code path governed by that specification. Nothing on this page describes it. See The 1.02 asset surface.