mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 14:43:08 +08:00
2ba58a8cbf
Fixes #4669. OPC 10000-3 §7.15 says the SourceNode of `GeneratesEvent` "shall be an ObjectType, a VariableType or a Method". CTT `Base Info Core Structure 2 001.js` flags every Object that is the source of one. #4667 fixed this in the source generator. This PR fixes the three remaining WoT places. ### 1. WotCon asset registry - `AssetRegistry.BuildEventNode` / `RemoveEventTypes` no longer add or remove `GeneratesEvent` from the asset Object. Event delivery already relies on `EventNotifier = SubscribeToEvents` and root-notifier registration. - Tests now assert the reference is **absent** and the notifier bit is set. The re-apply test asserts that the old EventType node is gone. ### 2. WoT NodeSet converter (`src/Opc.Ua.Types/Wot`) - **TD → NodeSet:** a last pass (`RelocateInstanceGeneratesEvent`) moves forward `GeneratesEvent` references off an Object/Variable root: - An event the bound type already declares needs nothing more. Matching is by qualified BrowseName against the local context's declarations, the same rule as the declaration merge. - Any other event is declared by a synthesized non-abstract `<rootLocal>Type`. It derives from the bound type (or from `BaseObjectType` / `BaseDataVariableType` when the TD binds none) and becomes the instance's `HasTypeDefinition`. A Variable root gets a `UAVariableType` with the instance's DataType and ValueRank. - Thing Models are unchanged: the projected type keeps its `GeneratesEvent`. - **NodeSet → TD:** - An instance's event affordances are now collected from its type definition and that type's supertypes. Instance-level `GeneratesEvent` is still read for third-party NodeSets. - In a single readable document, a type that only carries one instance's events (only `HasSubtype` + `GeneratesEvent`, typing exactly one instance) is not chosen as root. It is folded back: its supertype becomes the type binding, and no link is written when that supertype is the default base type. The TD → NodeSet → TD round trip stays readable, with no `uav:nodes` fallback. - A document set gives the carrier its own Thing Model, and the instance TD binds to it. - **Archive check:** an event affordance is accepted when the root's type definition, or one of its supertypes, generates it. - `docs/WoTNodeSetConversion.md` documents the behaviour in a new "Event source" row. ### 3. Aggregation sample - `SamplePump.NodeSet2.xml`: each pump is typed by its own `PumpType` subtype, `ns=1;s=Pump1.Type` (`Pump_1Type`) and `Pump2.Type`, which declares its alarm EventTypes. - It is per pump because each pump owns its alarm EventTypes. `Pump2.Type` is cloned with the rest of Pump2. - The pumps keep `EventNotifier="1"` and no longer carry `GeneratesEvent`. - The checked-in documents are regenerated, adding `sample-pump-pump-1type.json` / `-2type.json`. The pump TDs still list the alarm event affordances. - The sample binding lookup ignores Thing Models, because the type's TM declares the same EventType as the pump TD. A declaration missing from the instance still fails as before. - Docs: `WoTConnectivity.md` (asset event affordances, §12.4.2) and `WotBindings.md`. The `GeneratesEvent` clause in the sample-limitation note was already stale, so I removed it. ### Runtime check `WotNativeOwnershipIndex` only uses `GeneratesEvent` in its alias table. Projected event routing takes the owner from `uav:componentOf` / `uav:id` and the notifier from Parent/`HasNotifier`. Nothing in `Opc.Ua.WotCon.Server` reads an instance-level `GeneratesEvent`. ### Tests (net10.0) - `Opc.Ua.Types.Tests`: 11446 passed, 1 skipped. New `WotInstanceEventSourceTests` covers object and variable roots, TMs, the TD with no events, the folded round trip, the legacy instance reference and the document set. Two bound-type cases were added to `WotDeclarationMergeBoundaryTests`. - `Opc.Ua.WotCon.Bindings.Tests`: 714 passed. - `Opc.Ua.WotCon.Tests`: 1496 passed, including the byte-exact sample regeneration tests. - net48 build of both test projects is clean, and `dotnet format whitespace` is clean. Not run: CTT `Base Info Core Structure 2 001.js` (p02-base-info) against a WotCon server. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
881 lines
68 KiB
Markdown
881 lines
68 KiB
Markdown
# 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.
|
||
|
||
## Contents
|
||
|
||
- [Which specification revision this tracks](#which-specification-revision-this-tracks)
|
||
- [Importable output](#importable-output)
|
||
- [Modelling rules and the two placeholder identifiers](#modelling-rules-and-the-two-placeholder-identifiers)
|
||
- [WoT to NodeSet defaults](#wot-to-nodeset-defaults)
|
||
- [Portable identity and preservation semantics](#portable-identity-and-preservation-semantics)
|
||
- [Verified linked document sets](#verified-linked-document-sets)
|
||
- [Generated NodeIds follow Annex G.1](#generated-nodeids-follow-annex-g1)
|
||
- [Preservation digests and the two things that can be measured](#preservation-digests-and-the-two-things-that-can-be-measured)
|
||
- [Unmapped reference vocabulary is residue](#unmapped-reference-vocabulary-is-residue)
|
||
- [Condition events derive from their ConditionType](#condition-events-derive-from-their-conditiontype)
|
||
- [Event fields come from the type, not only from the document](#event-fields-come-from-the-type-not-only-from-the-document)
|
||
- [Resolving a stated selection needs the asynchronous conversion](#resolving-a-stated-selection-needs-the-asynchronous-conversion)
|
||
- [DataType definitions (Section 6.11)](#datatype-definitions-section-611)
|
||
- [Model and platform vocabulary (Section 6)](#model-and-platform-vocabulary-section-6)
|
||
- [Conformance claims and strict mode (Sections 4.1, 6.1, 6.6 and 11)](#conformance-claims-and-strict-mode-sections-41-61-66-and-11)
|
||
- [Authoring a claim and processing one are different acts](#authoring-a-claim-and-processing-one-are-different-acts)
|
||
- [The `uav:nodes` record grammar is not vocabulary](#the-uavnodes-record-grammar-is-not-vocabulary)
|
||
- [Generated documents state the revision they were generated against](#generated-documents-state-the-revision-they-were-generated-against)
|
||
- [NodeClass and ReferenceType vocabulary](#nodeclass-and-referencetype-vocabulary)
|
||
- [Engineering units, ranges and scaling (Sections 6.4 and 6.4.1)](#engineering-units-ranges-and-scaling-sections-64-and-641)
|
||
- [Localized text (Section 9.1.1)](#localized-text-section-911)
|
||
- [ValueRank and ArrayDimensions (Sections 7 and 9.1)](#valuerank-and-arraydimensions-sections-7-and-91)
|
||
- [Method arguments (Section 9.1)](#method-arguments-section-91)
|
||
- [ReferenceTypes and relations (Sections 5.1.2, 5.3 and 6.2)](#referencetypes-and-relations-sections-512-53-and-62)
|
||
- [`uav:componentOf` and its `ua:ComponentOf` alias](#uavcomponentof-and-its-uacomponentof-alias)
|
||
- [Projection documents and the OPC 10100-1 v1.02 asset surface](#projection-documents-and-the-opc-10100-1-v102-asset-surface)
|
||
|
||
## Which specification revision this tracks
|
||
|
||
This implementation tracks **WoT Binding revision 1.1**. The revision is
|
||
stated once, as `Opc.Ua.Wot.WotBindingConformance.CurrentRevision`, and
|
||
everything that depends on it — the `uav:bindingVersion` values strict
|
||
conformance accepts, the versioned artifact base
|
||
`http://opcfoundation.org/UA/WoT-Binding/v1.1/`, and the closed Section 11
|
||
name set — derives from that constant. No public behaviour is keyed to an
|
||
upstream commit: a revision is a published contract, a commit is not.
|
||
|
||
The `uav` prefix binds to `http://opcfoundation.org/UA/WoT-Binding/`. The
|
||
reader matches the compact `uav:` spelling and never consults the IRI a
|
||
document binds the prefix to, in permissive *and* in strict mode, because
|
||
strictness is about the vocabulary a document uses and not about the IRI it
|
||
declares that vocabulary under. A document written against the earlier
|
||
`http://opcfoundation.org/UA/WoT/v1#` draft binding therefore stays
|
||
readable; that leniency is deliberate and is covered by a test.
|
||
|
||
The specification's worked examples are vendored as test fixtures under
|
||
`tests/Opc.Ua.Types.Tests/Wot/Assets`, and *those* do record the exact
|
||
source. `spec-examples.manifest.json` beside them names the source
|
||
repository, branch and commit together with the size and SHA-256 of every
|
||
example, and `WotSpecFixtureManifestTests` fails the build when the two
|
||
disagree. See [How this is checked](WotBindings.md#how-this-is-checked)
|
||
for the drift check and the regeneration step.
|
||
|
||
## Importable output
|
||
|
||
A NodeSet2 document may write a standard name such as `HasComponent` or
|
||
`Double` wherever a NodeId is expected, but only if it declares that name
|
||
in its own `<Aliases>` table; the importer reports `BadNodeIdInvalid` for a
|
||
name it cannot find. Both halves of the conversion produce such names —
|
||
synthesis writes the readable names directly, and a restore reproduces
|
||
whatever spelling the document it restores from used — so every converted
|
||
NodeSet is passed through an alias-completion pass before it is returned.
|
||
That pass is not a WoT concept: `NodeSetAliasCompleter` sits beside
|
||
`UANodeSet` itself and declares whatever the `INodeSetAliasResolver` it is
|
||
handed knows. The WoT Binding's own policy is one object,
|
||
`WotNodeSetAliases`, which states that a converted document writes the
|
||
standard base-namespace names and delegates to the single table of them
|
||
(`NodeSetStandardAliases`) rather than repeating any.
|
||
|
||
The pass declares only names that policy resolves, appended after the
|
||
declarations the document already carried in ascending ordinal order of
|
||
the alias. It is idempotent, so a byte-exact `uav:nodeSet` restore stays
|
||
byte-exact, and it never rewrites a name: a vendor alias the document uses
|
||
but does not declare still fails the import with the message that names
|
||
it, rather than being quietly discarded.
|
||
|
||
Completion is a decision a *producer* makes about a document it writes.
|
||
Comparison makes none of its own: `NodeSetComparer.CompareEquivalent`
|
||
resolves each document through its own declarations first and asks
|
||
`NodeSetComparisonOptions.AliasResolver` about the rest. That property
|
||
defaults to nothing at all, so a comparison of two arbitrary NodeSets does
|
||
not report a document that writes `HasComponent` without declaring it —
|
||
which no Server could load — as equivalent to one that writes `i=47`. The
|
||
WoT conversion injects `WotNodeSetAliases` through
|
||
`WotNodeSetConverterOptions.ToComparisonOptions`, so its Section 9.2
|
||
completeness check reads the names the Binding writes, and the comparison
|
||
itself stays free of any knowledge of WoT.
|
||
|
||
That matters beyond tidiness, because the runtime registry materialization
|
||
path is `ConvertAsync` → serialize → `UANodeSet.Read` → `Import`. Every
|
||
vendored specification example that converts is run through exactly that
|
||
sequence by `WotNodeSetImportTests`, and so is every preservation mode.
|
||
|
||
### 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: root is a `UAObject` with `HasTypeDefinition` to `BaseObjectType` (`i=58`). Present: a `ua:HasTypeDefinition` link (WoT Binding Section 5.2.1) whose `href` is the ExpandedNodeId of the type, and/or a compact model name in `@type`, binds the root to that type so the converter reuses the existing type rather than defining a second one. The two forms are resolved against the Section 5.1.5 local context — the sibling documents of the conversion first, a loaded AddressSpace as the fallback — supplied through `IWotNodeResolver`. A binding that names a type the local context does not hold **fails** (`UnresolvedTypeBinding`) rather than falling back to `BaseObjectType`; two bindings that disagree, an ambiguous name with nothing to settle it, or a resolved type of the wrong NodeClass **fail** as `AmbiguousTypeBinding`. A compact name is a binding when its namespace is one the local context holds; any other `@type` member is ordinary annotation and is retained as residue. |
|
||
| 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: the Method is materialized with no argument Property. Present: the schema becomes an `InputArguments` / `OutputArguments` Property (`Argument[]`, `DataType` `i=296`, `ValueRank` `1`) held by `HasProperty`, with NodeId `ns=1;s=/nsu=<escaped model NamespaceUri>;<rootLocal>/nsu=<escaped model NamespaceUri>;<actionLocal>/<InputArguments\|OutputArguments>` (Annex G.1; the standard child keeps its bare base-namespace name). A schema that names one DataType — through `uav:mapToType`, `uav:dataTypeId`, `uav:dataTypeName` or an inline `uav:dataTypeDefinition` — is one argument, named from its `uav:browseName` or `title` and otherwise `Input` / `Output`; an object schema's members are the arguments. Each member's DataType resolves by the property rules above, `uav:valueRank` defaults to `-1` and `uav:arrayDimensions` and `description` are carried onto the `Argument`. Order comes from `uav:fieldOrder` (Section 6.11.4); a single member needs none, and an `Acknowledge`, `Confirm` or `AddComment` action takes the fixed OPC 10000-9 order `EventId`, `Comment`. A multi-member schema with no order **fails** (`MethodArgumentOrderAmbiguous`) rather than using JSON member order, and one whose `uav:fieldOrder` does not list every member exactly once, or that is not a JSON object, **fails** (`MethodArgumentSchemaInvalid`). A reported schema is still carried verbatim through residue, so a rejected document never loses the signature it authored. |
|
||
| 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 canonical
|
||
shortest round-trip spelling on every supported framework. This avoids
|
||
platform-specific formatter output being rejected as an over-precise authored
|
||
literal. Validation of received numeric literals remains unchanged.
|
||
|
||
```csharp
|
||
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 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 is what 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 and the two things that can be measured
|
||
|
||
Annex G distinguishes three measurements over a JSON value, and this
|
||
implementation keeps them apart. Two of them are digests; the third is a size.
|
||
|
||
* **A digest over retained bytes.** The `Sha256` of a `WoTJsonResidue` member is
|
||
the SHA-256 of the **decoded residue bytes exactly** — the bytes the producer
|
||
encoded, and the bytes a verifier decoded. Nothing is canonicalized, reordered,
|
||
re-escaped or re-formatted before digesting, in either direction, and a
|
||
mismatch is a mismatch: the entry is corrupt, not merely differently spelled.
|
||
A residue member exists to carry a value Section 6.6 forbids a consumer to
|
||
reformat, so a digest over a re-serialization would pin a value no party is
|
||
allowed to produce. The `sha256` of a `uav:nodeSet` envelope and the
|
||
`uav:sourceDigest` of a projection source are of the same kind.
|
||
* **Equality over a JSON value.** RFC 8785 (JCS) is used where two JSON *values*
|
||
are compared for equality — the Section 9.4 conflict test that asks whether a
|
||
residue entry holds the same value as a member the readable mapping already
|
||
produced. `WotJsonCanonicalizer` is that form, and `WotDocument.ToCanonicalUtf8`
|
||
writes it: object members sorted by the UTF-16 code units of their names,
|
||
the minimal string escaping of ECMAScript `JSON.stringify`, and numbers in the
|
||
ECMAScript form of their IEEE-754 double value. A reordered object, an
|
||
equivalent escape and `1.0` beside `1` are therefore one value and not a
|
||
conflict. A number outside the interoperable domain of RFC 8259 §6 — a literal
|
||
carrying more precision than a double holds, such as `9007199254740993` — is
|
||
**diagnosed** rather than canonicalized, and the conflict test falls back to
|
||
comparing the two as written: that can report a conflict JCS would not, but it
|
||
never reports two different values as one. No digest is taken over this form.
|
||
* **A size over the compact received form.** The opaque-object size bound of
|
||
Section 6.6 is measured over the **compact received form** of Annex G.4: the
|
||
received text of the value with insignificant whitespace removed — the four
|
||
RFC 8259 whitespace characters outside a string literal — and nothing else
|
||
changed. Member order is the received order, numbers keep the lexical form
|
||
they were written in, and strings keep the escapes they were written with.
|
||
Deliberately *not* a canonical re-serialization: measuring one would oblige a
|
||
consumer to produce exactly the reordered, renumbered value the preservation
|
||
rule forbids, and an "almost-JCS" measurement — compact separators but a
|
||
language's own `double` formatting — is what two implementations disagree
|
||
about in practice. Whitespace removal outside string literals is decidable by
|
||
a scanner with one bit of state, so two implementations that received the same
|
||
bytes measure the same number. The depth and key-count bounds are measured
|
||
over the parsed value, where formatting cannot matter.
|
||
|
||
### 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](WotBindings.md#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](WotBindings.md#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 `data` object 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](WotBindings.md#event-field-selection-tmref-and-uaveventselectclauses)).
|
||
|
||
### 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:
|
||
|
||
```csharp
|
||
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 `ToNodeSet` throws and
|
||
`ToNodeSetResult` reports the diagnostic. A well-formed value is
|
||
carried unchanged through the generic `uav:nodes` residue 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:nodes`
|
||
native 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 carries `uav:browseName` round-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 and the two things that can be measured](#preservation-digests-and-the-two-things-that-can-be-measured)),
|
||
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)
|
||
|
||
Four document-level terms describe the document rather than the
|
||
AddressSpace, so none of them becomes a Node and all of them 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 malformed claim stays an error in every mode: the
|
||
syntactic rule is what a consumer enforces, and it enforces it always.
|
||
|
||
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 unknown `uav:` 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.
|
||
- **`Strict`** additionally reports a `uav:` term this revision does not
|
||
define, an opaque object that breaks the key or bound rules, and —
|
||
where `RequiredConformance` names units or profiles — a claim the
|
||
document does not make. Claiming a profile claims every unit it names,
|
||
so `WoT-Modeller` satisfies a requirement of `WoT-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`
|
||
is the complete set of 113 `uav:` IRIs the published `@context` mints:
|
||
`IsKnownTerm` answers for the 100 a document spells with the prefix, and
|
||
`ScopedTerms` / `IsScopedTerm` for the 13 a scoped context mints under a short
|
||
member name (`namespaceUri`, `unitId`, `minimum`, `sha256` and the rest).
|
||
|
||
### 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:
|
||
|
||
- `EngineeringUnits` projects to a property affordance of its own. That
|
||
affordance carries `type: "string"` — what a client reads there at run time —
|
||
the definitive `uav:mapToType` `i=887`, and `uav:engineeringUnits` with the
|
||
`EUInformation`'s `namespaceUri`, integer `unitId`, `displayName` and
|
||
`description`. The authority and its machine-readable code are what a display
|
||
string alone cannot recover.
|
||
- The annotated Variable's affordance states `unit` (the `EUInformation`
|
||
`DisplayName`, never a quantity kind) and `uav:unitProperty`, a canonical
|
||
`/properties/<name>` pointer at that sibling affordance.
|
||
- `EURange` becomes `minimum` and `maximum`; `InstrumentRange` becomes
|
||
`uav: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.** See the `input` / `output` row of the defaults table above.
|
||
|
||
## 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 is what 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](WotBindings.md#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](WoTConnectivity.md#124-projection-documents-and-the-view-nodeclass).
|
||
- The **OPC 10100-1 v1.02 asset surface** (`CreateAsset`, `WoTFile` upload, the
|
||
`AssetRegistry` POCO reader) is a separate code path governed by that
|
||
specification. Nothing on this page describes it. See
|
||
[The 1.02 asset surface](WoTConnectivity.md#126-the-102-asset-surface).
|