Skip to content

Add OPC UA Robotics companion SDK (tracking: split into #4123-#4127) - #4096

Closed
marcschier wants to merge 55 commits into
masterfrom
marcschier/robotics-3827-api
Closed

Add OPC UA Robotics companion SDK (tracking: split into #4123-#4127)#4096
marcschier wants to merge 55 commits into
masterfrom
marcschier/robotics-3827-api

Conversation

@marcschier

@marcschier marcschier commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

This PR has been split into five stacked, individually reviewable PRs and is now a tracking issue. Please review the five below rather than this one.

The branch behind this PR is unchanged and remains the integration reference. #4127 is content-identical to it, so merging the stack in order lands exactly what this PR contained.

The stack

Merge in order; each targets the one above it.

# PR Scope Size
1 #4123 Core stack — reusable IFileDirectoryBinder, the IGeoLocationProvider seam, FiniteStateMachineDispatcher, NodeId-factory and JSON decoder fixes, build-matrix restriction ~4.5k
2 #4124 Source generator — typed NodeSet method signatures, referenced-model fluent accessors, the method-declaration and StateNumbers fixes ~4.1k
3 #4125 Positioning — OPC 10000-210 RSL + 10000-211 GPOS, ISA-95 onto the shared seam, MinimalIsa95Server ~8k
4 #4126 OpenUSD — binding, scene materialization, connector + viewport, PumpDeviceIntegrationServer ~30k
5 #4127 Robotics — the OPC 40010 SDK, client, and MinimalRobotServer. Closes the API work for #3827 ~32k

No PR depends on a sibling's feature; the linear order is a review convenience. The only hard constraints are that everything needs 1 and 2, and MinimalRobotServer composes Robotics + OpenUSD + Positioning, so it can only exist at the top.

Why it was split

53 commits, ~390 files, ~84k insertions bundling six unrelated concerns. Any single objection blocked all of it, and the diff was not reviewable in one pass.

What this delivers overall

Implements the OPC UA Robotics companion SDK requested in #3827, together with the OpenUSD and positioning work it builds on:

  • Model foundation — source-generated OPC 40010 Robotics 1.02 and OPC 40001-1 IA over source-generated OPC UA DI, with vendor subtyping supported end to end.
  • Robotics — stock hosting, fluent topology builders, Part 16 standard operations, a FileDirectoryType-backed Programs directory, opt-in non-normative motion verbs, and a subtype-aware client that resolves verbs by BrowseName so no NodeId mapping file is needed.
  • OpenUSD — a generic OPC UA to OpenUSD binding and scene materialization, plus a connector that drives a live stage, with an optional in-process viewport.
  • Positioning — OPC 10000-210 / 10000-211, with one geolocation provider serving both GPOS and ISA-95.

Defects found and fixed

  • ControllerType.CurrentUser is Mandatory in OPC 40010 but was neither built nor validated.
  • MotionDeviceType.TaskControlReference was absent because it must target a TaskControlOperationType instance that did not exist.
  • StateNumbers / TransitionNumbers emitted empty for every NodeSet-sourced model — a Part 16 defect well beyond Robotics.
  • Node copies consumed NodeIds from the NodeIdFactory, so a counting factory silently burned identifiers and a tracking one leaked them.
  • Method declarations became self-referential, so Call with a supertype's MethodId returned Bad_MethodInvalid — 10 Alarms and Conditions and 370 GDS conformance failures.
  • ISA-95 GeoSpatialLocation was written as a scalar where OPC 10030 §8.3.2.2 declares OneOrMoreDimensions.
  • A geolocation provider that cannot push served its start-up sample with Good status forever.
  • Security: file system entry names allowed traversal past the provider check on Windows; Robotics state transitions could commit concurrently; the USDA writer emitted unescaped values that could author their own layer syntax; the connector kept private state in the shared temp directory.
  • Measurement Variables wired with only an OnRead getter never raise a data change, so a subscribed twin rendered the start-up value forever.

Related issues

Closes the API work for #3827. Raised #4099 (generated CreateOrReplace<Child> helpers should assign instance NodeIds) while addressing review feedback; that landed on master as #4104 and is merged in here.

Checklist

  • I have signed the CLA and read the CONTRIBUTING doc.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added all necessary documentation.
  • I have verified that my changes do not introduce (new) build or analyzer warnings in the validated project set.
  • 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.

marcschier and others added 21 commits July 20, 2026 17:57
Enhancements to the model source generator so that a *compiled* companion
NodeSet2 (e.g. an OPC companion spec such as Robotics/IA, or the OpenUSD
binding model) can be source-generated:

- Add OpcUaStateTypeIndex and wire it through ModelCompilation /
  ModelSourceGenerator so standard (ns=0) node-state/method-state class
  names are resolved from the referenced Core(.Types) metadata.
- Add StandardMethodStateFallback: when a compiled NodeSet instantiates a
  standard type that carries methods (e.g. a FileDirectoryType 'Delete'
  instance), the default naming can compute an 'Opc.Ua.*MethodState' name
  that Core never emits under that name. Degrade an unresolved standard
  'global::Opc.Ua.*MethodState' reference to the base 'global::Opc.Ua.
  MethodState' only when that typed class is neither declared by the
  current pass nor present in the compilation, keyed on the exact Opc.Ua
  namespace and never entered by the Stack pass that builds Core.Types.
  This is the scoped interim workaround tracked by
  #4060 (the proper upstream fix is a
  name-resolution step, which this makes unnecessary once landed).
- NodeStateGenerator / ModelDesignExtensions / FluentBuilderGenerator:
  supporting changes for the above.
- Opc.Ua.Types: expose internals to Opc.Ua.Core.Encoders.Tests.

Validated: generator builds (netstandard2.0), Opc.Ua.Core.Types generates
and builds, and the full Opc.Ua.SourceGeneration.Core test suite passes
(3715 passed / 8 skipped / 0 failed).
Adds the OPC 40010 Robotics companion model and thin client/server helpers,
source-generated from the compiled Robotics/IA NodeSet2 files:
- Opc.Ua.Robotics: model + namespaces (Opc.Ua.Robotics.NodeSet2.xml,
  Opc.Ua.IA.NodeSet2.xml).
- Opc.Ua.Robotics.Client / Opc.Ua.Robotics.Server: browse/author helpers.

Depends on the source-generator support for compiled companion NodeSets in
the parent commit.
…es, e2e)

Implements the draft OPC UA — OpenUSD Bindings so a live OPC UA server can be
bound to an OpenUSD model to render live data:
- Opc.Ua.OpenUsd: the OpenUSD binding companion model.
- Opc.Ua.OpenUsd.Server: server-side representation authoring + asset delivery.
- Opc.Ua.OpenUsd.Client: the connector (discovery, composition/aggregation,
  intent bindings, asset streaming) and USD sinks.
- tools/Opc.Ua.OpenUsd.Connector: a standalone connector runner.
- samples/PumpDeviceIntegrationServer, samples/MinimalRobotServer: pump and
  robot integration servers demonstrating 1:1/Many composition, cross-server
  federation, dynamic composition, and asset content delivery.
- tests/Opc.Ua.Di.Tests: pump & robot OpenUSD end-to-end tests.

Wires the new projects into UA.slnx and Opc.Ua.Di.Tests. The dynamic-composition
pump e2e (DynamicPumpIsComposedThenDeactivatedAsync) is [Ignore]'d pending the
upstream browse-consistency issue #4061.
…e+owned-session close, asset size caps, serialized recompose
…, ITelemetryContext + source-generated logging
Resolved conflicts:
- Source generator (StandardMethodStateFallback/NodeStateGenerator/ModelSourceGenerator/
  FluentBuilderGenerator/ModelDesignExtensions/ModelCompilation/OpcUaStateTypeIndex): took
  master's #4063 (canonical merged 'compiled companion models' implementation), which
  correctly generates the OpenUSD/Robotics companion models.
- PumpDeviceIntegrationServer: kept the OpenUSD demo wiring (UpdateAlarmActive/representation),
  dropped the device-health feature that master removed; captured the PumpState returned by
  master's MaterialisePumpInstanceAsync instead of the removed m_pump1 field.
- Opc.Ua.Di.Tests.csproj: unioned net472 Compile-Remove entries.
Validated: Di.Tests + both samples + OpenUsd/Robotics libs build clean on net10.0; unit tests green.
…f<Type> factories

Learns from master #4066 (which fixed the Pump sample to create Pump #2 as the generated
PumpType instead of a bare DeviceState): the Robot sample created MotionDeviceSystem/
Controller/MotionDevice/Axis as bare BaseObjectState + a type-definition reference, so the
instances lacked the companion-type's mandatory structure. Now uses
CreateInstanceOfMotionDeviceSystemType/ControllerType/MotionDeviceType/AxisType and the
typed mandatory containers (MotionDevices/Controllers/Axes), keeping the demo signals and
OpenUSD bindings (referenced by NodeId). Removed the unused RoboticsServer.CreateTypedObject
library helper that embodied the same anti-pattern. All 11 RobotOpenUsdE2eTests pass.
…into marcschier/robotics-3827-api

# Conflicts:
#	samples/MinimalRobotServer/RobotCell.cs
#	src/Opc.Ua.OpenUsd.Client/OpenUsdConnector.cs
#	tests/Opc.Ua.Di.Tests/RobotOpenUsdE2eTests.cs
@marcschier marcschier changed the title Add OPC UA Robotics companion SDK with OpenUSD and positioning integration Add OPC UA Robotics companion SDK Jul 25, 2026
Comment thread docs/DependencyInjection.md
…-3827-api

# Conflicts:
#	tests/Opc.Ua.Di.Tests/Opc.Ua.Di.Tests.csproj
Adds the missing docs/Robotics.md developer guide for the
Opc.Ua.Robotics / Opc.Ua.Robotics.Server / Opc.Ua.Robotics.Client
package trio, covering the source-generated OPC 40010 model foundation,
AddRobotics / AddRoboticsModel / ConfigureRobotics(For) hosting,
IRoboticsModelProvider and IRoboticsConfigurator, the build context, the
validated fluent topology builders, value binding, semantic references,
validation rules, custom node managers and non-DI hosting, the common
ArrayOf<T> snapshot contracts, and RoboticsClient discovery.

Splits the combined docs index entry into separate OpenUSD and Robotics
entries, resolves the dangling docs/OpenUsd.md link to Robotics.md, adds
Robotics rows and a Robotics section to the dependency-injection guide,
lists IA and Robotics in the root README companion-spec coverage, and
links the new guide from the three Robotics NuGet READMEs and the
MinimalRobotServer sample README.
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.96811% with 172 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.04%. Comparing base (4251fec) to head (512feae).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
src/Opc.Ua.OpenUsd.Client/OpenUsdConnector.cs 91.13% 32 Missing and 38 partials ⚠️
....Ua.OpenUsd.Client/OpenUsdConnector.Composition.cs 84.57% 17 Missing and 12 partials ⚠️
src/Opc.Ua.OpenUsdScene.Server/UsdSceneExporter.cs 90.04% 0 Missing and 20 partials ⚠️
src/Opc.Ua.OpenUsd.Server/UsdAssetDelivery.cs 89.89% 1 Missing and 9 partials ⚠️
...5.Server/Builders/Isa95GeoSpatialLocationBinder.cs 82.35% 3 Missing and 3 partials ⚠️
...c/Opc.Ua.OpenUsd.Client/OpenUsdConnector.Assets.cs 95.68% 3 Missing and 3 partials ⚠️
src/Opc.Ua.OpenUsd.Client/VariantConversions.cs 89.83% 4 Missing and 2 partials ⚠️
src/Opc.Ua.OpenUsd.Client/UsdFileSink.cs 97.44% 0 Missing and 5 partials ⚠️
...UsdScene.Server/UsdSceneMaterializer.Properties.cs 98.56% 1 Missing and 4 partials ⚠️
...pc.Ua.Di.Server/Hosting/DiAddressSpaceOwnership.cs 20.00% 4 Missing ⚠️
... and 6 more
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           master    #4096       +/-   ##
===========================================
+ Coverage   64.02%   80.04%   +16.02%     
===========================================
  Files        1478     1613      +135     
  Lines      206272   221371    +15099     
  Branches    35532    38097     +2565     
===========================================
+ Hits       132057   177202    +45145     
+ Misses      61686    30907    -30779     
- Partials    12529    13262      +733     
Files with missing lines Coverage Δ
src/Opc.Ua.Client/FileSystem/FileSystemClient.cs 71.59% <100.00%> (+0.14%) ⬆️
.../Opc.Ua.ISA95.Server/Builders/Isa95ModelBuilder.cs 84.61% <100.00%> (+0.34%) ⬆️
src/Opc.Ua.OpenUsd.Client/CompositeUsdSink.cs 100.00% <100.00%> (ø)
...c/Opc.Ua.OpenUsd.Client/OpenUsdConnectorOptions.cs 100.00% <100.00%> (ø)
...a.OpenUsd.Server/OpenUsdRepresentationAuthoring.cs 100.00% <100.00%> (ø)
...Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.cs 95.23% <ø> (ø)
...pc.Ua.OpenUsdScene/Conversion/UsdSceneSignature.cs 76.40% <ø> (ø)
...Opc.Ua.OpenUsdScene/Conversion/UsdValueCoercion.cs 45.29% <ø> (ø)
.../Opc.Ua.OpenUsdScene/Conversion/UsdValueTypeMap.cs 96.51% <ø> (ø)
src/Opc.Ua.OpenUsdScene/Conversion/UsdaReader.cs 81.60% <ø> (ø)
... and 103 more

... and 526 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread docs/WhatsNewIn2.0.md
Comment thread src/Opc.Ua.Robotics.Server/Opc.Ua.Robotics.Server.csproj
Comment thread docs/Profiles.md
Comment thread docs/Robotics.md
Comment thread docs/OpenUsdScene.md Outdated
Comment thread samples/MinimalRobotServer/MobileRobotPositionOptions.cs
Comment thread samples/MinimalRobotServer/MobileRobotPositionOptions.cs
Comment thread tools/Opc.Ua.OpenUsd.Connector.Viewer/Opc.Ua.OpenUsd.Connector.Viewer.csproj Outdated
Comment thread tools/Opc.Ua.OpenUsd.Connector/Opc.Ua.OpenUsd.Connector.csproj
Comment thread src/Opc.Ua.Robotics/RoboticsContracts.cs Outdated
Comment thread src/Opc.Ua.Robotics/RoboticsNamespaces.cs Outdated
Comment thread src/Opc.Ua.Robotics/RoboticsNamespaces.cs Outdated
Comment thread src/Opc.Ua.Positioning.Server/Providers/IGlobalPositionProvider.cs Outdated
Comment thread src/Opc.Ua.Positioning.Server/PositioningAddressSpaceBuilder.cs Outdated
marcschier and others added 13 commits July 29, 2026 09:17
Since #4104 the generated CreateOrReplace<Child> helpers assign a per-instance
NodeId whenever the system context carries a NodeIdFactory. NodeState.Initialize
materialises the children of a copy through CreateChild, which goes through that
same plumbing - but the copy overwrites every child NodeId from its source right
afterwards, so each of those assignments is discarded.

For a counting factory that silently burns identifiers. For a factory that
tracks outstanding allocations it leaks them: booting the Robotics node manager
left ten reservations behind, held by nodes that no longer carried the reserved
NodeId, taken while the generated model copied the AddComment/Acknowledge and
CreateDirectory/CreateFile/MoveOrCopy argument properties of the ConditionType
and FileDirectoryType declarations. Those ten identifiers were then skipped by
every later allocation.

The assignInstanceNodeIds flag cannot carry the intent here, because it is not
part of the virtual FindChild contract the copy passes through - the declaration
factories already pass false and it is lost at NodeState.Create. Hide the
factory from the copy instead, via a context wrapper that forwards everything
else; the existing assignment sites already treat a missing factory as "do not
assign".

Also refreshes the pump NodeId surface baseline, which had not seen the OpenUSD
representation, component and signal nodes the sample adds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
- Drop the hand-written `RoboticsNamespaces` constants: the source generator
  already emits `Opc.Ua.Robotics.Namespaces` and `Opc.Ua.IA.Namespaces`, which
  the server code was using all along. Callers now use the generated classes.
- Split `RoboticsContracts.cs` into one record per file under `Contracts/`,
  leaving the namespace unchanged.
- Rename the sample's `RobotMobilityOptions` to `MobileRobotPositionOptions`
  (file and class) so it pairs with `MobileRobotPositionProvider`, and document
  every member of it and of `RobotMotionOptions` / `RobotMotionMode`.
- Document every member of the internal `IFileSystemHost` interface.
- Expand the 38 single-line `<summary>` tags left in the PR so the text sits on
  its own line between the tags.
- Fold `docs/OpenUsdScene.md` into `docs/OpenUsd.md` as a "Part 2 - scene
  materialization" section, keeping the Part 1 / Part 2 comparison up front, and
  update the entry in `docs/README.md`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
`Opc.Ua.Robotics.Client.Tests` + `Opc.Ua.Robotics.Server.Tests` become
`Opc.Ua.Robotics.Tests` (102 tests), and `Opc.Ua.OpenUsd.Client.Tests` +
`Opc.Ua.OpenUsdScene.Tests` become `Opc.Ua.OpenUsd.Tests` (393 tests). Each new
project takes the union of the old package and project references, and the
OpenUSD one keeps the frozen `Assets` fixture plus the linked MinimalRobotServer
assets.

The runtime budget the review asked about is comfortable: the combined OpenUSD
suite finishes in 2 s, and the Robotics one in 13 s, on net10.0.

The per-file namespaces are deliberately left alone. They resolve their product
types through the enclosing namespace, so folding them into a single test
namespace only forces a pile of new using directives for no benefit.

`InternalsVisibleTo` in the five affected src projects, `UA.slnx`, and the
MinimalRobotServer README now name the merged assemblies. The CI matrix
discovers test projects from the filesystem, so it needs no change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
Review preference: when an assignment wraps, the method name belongs on the same
line as the assignment target and the arguments one indent layer in, rather than
the call and its arguments each getting their own extra level.

Applied to the 44 places in this PR where the joined line stays inside the
120-column limit. Two call sites would overflow and keep the old shape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
A server author with one GPS source had to write two unrelated providers:
IGlobalPositionProvider for OPC 10000-211 (GPOS) and
IIsa95GeoSpatialLocationProvider for OPC 10030 (ISA-95). They disagreed on
value shape, keying, push semantics and timestamp type.

They cannot share a value type: OPC 10030 GeoSpatialLocationType is
DataType=String with ValueRank OneOrMoreDimensions, while GPOS
GlobalLocationDataType is a structure. The mismatch is directional though -
coordinates project onto text, not the reverse - so one provider can speak the
richer model and let ISA-95 format it.

Adds IGeoLocationProvider to Opc.Ua.Server (namespace Opc.Ua, folder Location/)
with a neutral GeoLocationSample: an optional GeoPosition (lat/lon plus optional
height, accuracy, floor and EPSG code), an optional GeoOrientation, and optional
text Labels. The contract names no companion-model type, so Opc.Ua.Server keeps
referencing only core assemblies - the same shape IFileSystemProvider already
uses. Ships InMemoryGeoLocationProvider and a WKT formatter alongside it.

Both consumers now bind that one contract. GPOS maps a sample onto
GlobalLocationDataType, requiring a position and rejecting a sample whose EPSG
code contradicts the Variable's configured CoordinateReferenceSystem rather than
silently mis-georeferencing it. ISA-95 projects the sample to text through an
injectable IGeoLocationTextFormatter.

Two behaviour changes fall out. A source that only knows a textual location
("Building 4, Detroit Plant") stays first class for ISA-95 without fabricating
coordinates. And the ISA-95 Variable now publishes a String array, fixing a
pre-existing conformance gap against the spec's ValueRank - which is also what
lets it carry a WKT geometry and a human-readable literal together.

Providers declare SupportsPush instead of signalling "no push" by returning a
null stream; the binder only subscribes when it is true.

Both old interfaces are deleted rather than obsoleted, since 2.0 is unreleased.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
The IGeoLocationProvider contract and docs both state that when SupportsPush is
false the host polls ReadAsync at the bound Variable's sampling interval. The
ISA-95 binder honoured that through its async read hook, but the GPOS binder
substituted Task.CompletedTask and installed neither a hook nor a poll loop, so
a GlobalPosition or GlobalLocation Variable bound to a non-push provider served
its initial sample with Good status indefinitely - stale data indistinguishable
from live data.

Adds the missing poll loops, driven by the Variable's MinimumSamplingInterval
and falling back to one second, with the same source validation, failure
handling and cancellation semantics as the push path.

Found by a security review of 6a63237, which flagged it as a contract
mismatch rather than an exploitable flaw. The regression test fails against the
previous behaviour.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
Security review of this PR surfaced four MEDIUM findings; all four are fixed
here, three with regression tests that were verified to fail without the fix.

1. FileSystem: path traversal via entry names.
   CreateDirectoryAsync, CreateFileAsync and MoveOrCopyAsync passed the
   client-supplied name straight to the provider. The contract promises
   forward-slash provider-relative paths but never enforced it, so the obvious
   provider-side check (split on '/', reject '..') is bypassed on Windows by a
   backslash payload. IsValidEntryName now rejects separators, drive colons,
   '.' and '..' and blank names before the provider is reached. Defence in
   depth: the shipped PhysicalFileSystemProvider was already safe.

2. Robotics: concurrent state transitions.
   The three Invoke* methods ran guard -> handler -> commit with no mutual
   exclusion, so two concurrent calls could both pass the guard and commit
   conflicting states. Serialised with an Interlocked.CompareExchange gate
   (not SemaphoreSlim - the builder is not disposable, so that trips CA1001)
   and a try/finally release.

3. OpenUSD: USDA injection.
   UsdaWriter emitted string values, composition-arc references, variant set
   names and selections, doc and prim names without escaping, so a value
   carrying a quote or newline could author its own layer syntax. Added
   EscapeQuoted, EscapeAssetPath, EscapeTripleQuoted and IsAssetPathUnsafe and
   routed every emit site through them.

4. Connector: private state under the shared temp directory.
   The asset cache and PKI root both lived under Path.GetTempPath().
   GetPrivateStateRoot() now uses LocalApplicationData, falling back to
   AppContext.BaseDirectory.

Also corrects PumpInstanceNodeIdRegressionTests. The baseline pinned six
InputArguments/OutputArguments nodes on argument-less standard methods
(Acknowledge, AddComment, Disable, Enable) as a deliberate deviation. They are
absent after a clean source-generator rebuild: the baseline encoded a
stale-generator artifact, not real behaviour. The nodes and the comment
explaining them are removed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
The previous commit removed six InputArguments/OutputArguments entries for
argument-less standard condition methods, on the theory that they were a
stale-source-generator artifact. That was wrong. A clean rebuild of the
generators plus Opc.Ua.Core.Types reproduces them: Opc.Ua.Identifiers.g.cs
really does declare AcknowledgeableConditionType_Disable_InputArguments (25785)
and _OutputArguments (25786), so the nodes are genuine generator output on this
branch. The run that appeared to drop them was the stale build.

The entries and the comment explaining the deviation are restored.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
…ature

Calling a Method on an Object may use the NodeId of the Method declaration in
the ObjectType or any supertype (OPC 10000-4, Call). The server resolves that
through MethodState.MethodDeclarationId, so an instance's Enable must still
carry ConditionType_Enable (i=9027) even when the alarm is a
NonExclusiveDeviationAlarmType.

UpdateMergedInstance pointed MethodDeclarationNode at the merged override
itself whenever the override specified a type, arguments or a declaration.
For AcknowledgeableConditionType_Enable - which overrides ConditionType_Enable
with the same (empty) signature - that made MethodDeclarationId self
referential, so FindMethod no longer matched the supertype MethodId and Call
returned BadMethodInvalid. Ten Alarms and Conditions conformance tests in
Opc.Ua.History.Tests failed as a result (Enable, Disable, Acknowledge, Confirm
and AddComment on every alarm subtype).

The inherited declaration is now preserved when the override keeps the
inherited signature. A genuine signature change still makes the override its
own declaration, which is what DerivedMethodTypeOverrideUsesStringDefinition-
Everywhere pins. GetMergedInstance likewise falls back to the instance
declaration it copied rather than to the copy.

Opc.Ua.History.Tests: 496 passed / 10 failed -> 506 passed / 0 failed.
Opc.Ua.SourceGeneration.Core.Tests: 3739 passed / 0 failed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
DirectoryObjectState now builds its BrowseName in the namespace of its NodeId
instead of namespace zero, which is correct: the directories and files a
provider exposes are server defined nodes, not standard ones. FileSystemClient
was still translating an unqualified path segment as a namespace zero
BrowseName, so TranslateBrowsePathsToNodeIds no longer matched anything and
GetInfoAsync, ExistsAsync, FileExistsAsync and DirectoryExistsAsync all
returned null or false for paths that exist. Browsing was unaffected because
DirectoryBrowser only compares namespace indexes when a specific child is
requested.

A segment written without a prefix is now qualified with the namespace of the
parent it is resolved against. A segment that already carries an index, such as
"1:foo", keeps it.

Opc.Ua.Features.Tests: 132 passed / 1 failed -> 133 passed / 0 failed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
build-linux-all-tfm builds UA.slnx once per framework with
/p:CustomTestTarget=<tfm>. That only redirects the shared TFM variables in
targets.props, so a project that hardcodes a single TargetFramework keeps
compiling against its own reference assemblies while the rest of the stack is
built for <tfm>. Opc.Ua.OpenUsd.Connector.Viewer is pinned to net10.0, so the
net8.0 leg compiled it against System.Runtime 10.0 while linking an
Opc.Ua.Types built for net8.0, and the System.Threading.Lock polyfill collided
with the real type:

  UsdStageSink.cs(109,26): error CS0433: The type 'Lock' exists in both
  'Opc.Ua.Types' and 'System.Runtime'

The RestrictForLegacyTfm no-op already exists for exactly this situation but
only covered net472/net48/netstandard2.0/netstandard2.1. It now also applies
when an opted-in project pins one TargetFramework and CustomTestTarget names a
different one, which covers the net8.0 and net9.0 legs as well. Multi-targeted
projects follow CustomTestTarget through targets.props and are unaffected, as
are builds that pass no CustomTestTarget.

Verified: UA.slnx Release builds with 0 errors for net8.0 (previously failed),
net9.0, net10.0 and netstandard2.1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
Codecov reported 72.16% patch coverage against an 80% target, with the whole
remaining gap in the new OpenUSD sources. The connector, its asset and
composition partials, the file and composite sinks, the scene materializer,
exporter and discovery, the asset delivery service and the representation
authoring helpers are now exercised directly.

The enabler is FakeSessionHarness: an in-memory address space serving
Browse/Read/Call with access levels, role permissions, fault injection and Part
5 file streaming. ManagedBrowseAsync and the convenience CallAsync overload are
extension methods and cannot be mocked, so the harness implements the
underlying interface methods instead.

Also adds an integration test that a namespace qualified file system path such
as "3:name" resolves to the same node as the unqualified form, covering the
segment qualification added with the provider namespace fix.

238 new tests, Opc.Ua.OpenUsd.Tests 397 -> 635, all passing, 0 warnings.
Measured line coverage over the twelve target files rises from 78.4% to 96.8%.

Not covered, because it would need production changes rather than tests:
unreachable arms in VariantConversions, UsdAssetDelivery, UsdSceneMaterializer
and CompositeUsdSink, and the StartAsync subscription lifecycle plus the
private static compose helpers in OpenUsdConnector, which need a live
subscription pipeline rather than a fake.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
Resolves the PumpDeviceIntegrationServer simulation conflict. Both sides added
a pump simulation: master (#4116) generalised it into a per-pump
PumpSimulationState driven through IValueUpdater with phase profiles and
multi-pump registration, while this branch turned the pump into an EN 733 twin
that drives the OpenUSD viewport.

Master's structure is kept because IValueUpdater.SetValue is the general
mechanism for the defect this branch worked around with its own publish loop -
a variable wired with only OnRead never raises a data change. The branch's twin
specifics are re-expressed inside it: PumpSimulationState now integrates the
shaft angle from the running flow and exposes ShaftAngleDegrees and
AlarmActive, and the node manager publishes the OpenUSD signals that are
created outside the fluent builder and so have no updater of their own.

Also reconciles the pump NodeId baseline. The branch pinned six
InputArguments/OutputArguments nodes on argument-less condition methods that
the source generator emitted before the method declaration fix earlier on this
branch; master's baseline never had them and the merged address space no longer
produces them, so they are dropped. Every one of master's 80 entries and all of
the branch's OpenUSD representation entries are preserved.

JsonDecoder merged cleanly - the two sides changed different methods.

Validated: PumpDeviceIntegrationServer builds with 0 warnings,
Opc.Ua.Di.Tests 342 passed / 0 failed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
… the PR

Splitting this branch into the #4123-#4127 stack surfaced two defects that a
net10.0-only test run had hidden.

UsdAssetDeliveryTests used the static SHA256.HashData, which does not exist on
the net472 and net48 targets this test project also builds for, so the solution
build failed for those frameworks. It now hashes through SHA256.Create like the
production code it exercises.

The pump NodeId baseline dropped six InputArguments/OutputArguments entries for
argument-less standard condition methods during the merge with master. That edit
was validated against a stale incremental build; a clean rebuild of the
generators shows the nodes really are emitted, so the entries are restored and
the deviation stays visible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
marcschier added a commit that referenced this pull request Jul 31, 2026
…e machines (#4123)

**Stack 1/5** — base `master`. Part of the #4096 split; see #4096 for
the overall picture.

## What this is

The core stack changes the Robotics, OpenUSD and Positioning work
depends on. No new projects; every change is to an existing one, so this
is reviewable without any of the companion specs.

## Contents

**`Opc.Ua.Server`**
- `FileSystem` gains a reusable `IFileDirectoryBinder` and
`IFileSystemHost`, so any node manager can serve a Part 5
`FileDirectoryType` over `IFileSystemProvider` instead of that binding
living inside one companion model.
- Entry names are validated before they reach the provider. The contract
promises forward-slash provider-relative paths but never enforced it, so
a backslash payload such as `..\..\..\Windows\System32\evil.txt`
bypassed the obvious provider-side check on Windows. Defence in depth —
the shipped `PhysicalFileSystemProvider` was already safe.
- `Location` adds a single `IGeoLocationProvider` seam so one provider
implementation serves both OPC 10000-211 GPOS and OPC 10030 ISA-95. The
two specs disagree on representation, not on content, so the difference
is a projection rather than a reason to write the provider twice. Ships
a WKT text formatter and an in-memory provider.
- `StateMachines` adds `FiniteStateMachineDispatcher`, the Part 16
transition guard used by DI software update and later by Robotics task
control.

**`Opc.Ua.Types`**
- Node copies no longer consume NodeIds from the `NodeIdFactory`.
`Initialize` materialises children through `CreateChild`, which assigns
a per-instance NodeId, but the copy then overwrites every child NodeId
from its source, so the assignment is discarded. A counting factory
silently burned identifiers; a tracking one leaked them.
- `JsonDecoder` resolves a relative encodeable type id against the
namespace table before the factory lookup.

**`Opc.Ua.Client`** — a file system path segment written without a
namespace prefix is qualified with the namespace of the parent it is
resolved against, matching the provider-owned BrowseNames.

**`Directory.Build.targets`** — the `RestrictForLegacyTfm` no-op now
also applies when a project pins a single `TargetFramework` and
`CustomTestTarget` names a different one. That is the same situation it
already handled for the legacy frameworks, and it is what keeps the
per-TFM CI matrix green once net10.0-only projects appear later in the
stack.

## Validation

`dotnet build UA.slnx` — 0 errors.

| Suite | Result |
| --- | --- |
| Opc.Ua.Types.Tests | 8088 / 0 |
| Opc.Ua.Server.Tests | 3873 / 1 |
| Opc.Ua.Client.Tests | 2105 / 0 |
| Opc.Ua.Core.Tests | 4145 / 0 |
| Opc.Ua.Features.Tests | 134 / 0 |

The single `Opc.Ua.Server.Tests` failure is
`ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`,
which is pre-existing and environmental: the PKI path in my working
directory exceeds MAX_PATH. It reproduces on clean `master` and passes
from a shorter directory.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
marcschier added a commit that referenced this pull request Jul 31, 2026
Last in the #4096 stack, on top of the OpenUSD change. Closes the API work for

OPC 40010 1.02 defines a rich robot topology but no motion verbs: its actuation
surface is the SystemOperation and TaskControl state machines plus a Controller
Programs directory. This ships the model, the server hosting and the client in
two clearly separated tiers, and never presents the second as standard.

- Fluent topology builders for systems, controllers, motion devices, axes,
  power trains, motors, gears, drives, safety states, software, task controls
  and task modules, with the OPC 40010 semantic references.
- Standard operations drive the Part 16 state machines. An illegal cause is
  rejected with BadInvalidState before the handler runs, a handler returning a
  bad ServiceResult does not move the machine, and a transition already in
  flight rejects a concurrent one rather than letting both commit.
- Programs binds the optional FileDirectoryType through the reusable
  IFileDirectoryBinder added at the bottom of this stack.
- Operation conventions (MoveTo, Grasp, Release and friends) are opt-in and
  explicitly non-normative. They are created only in an application owned
  namespace; the OPC UA, DI, IA and Robotics namespaces are rejected.
- RoboticsClient is subtype aware throughout and resolves verbs by BrowseName,
  so no NodeId mapping file is needed.
- MinimalRobotServer composes five models purely through IRoboticsModelProvider
  contributions, which is the proof that the stock hosting is sufficient.

Also restores the vendor subtype scanner test, which uses the Robotics NodeSet
as its fixture and so could only land with this change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
marcschier added a commit that referenced this pull request Aug 1, 2026
…l accessors (#4124)

**Stack 2/5** — base #4123. Part of the #4096 split; see #4096 for the
overall picture.

> Review after #4123. The diff shown against `master` will include #4123
until that merges.

## What this is

Source generator only; no runtime library changes.

## Contents

- **Typed NodeSet method signatures.** A method imported from a NodeSet
now gets a typed signature. Methods that share a declared signature are
grouped onto one generated `MethodType`, named after its owner when the
plain name would collide, so two unrelated `Execute` methods no longer
share a class.
- **An override that keeps the signature it inherits keeps the
declaration it overrides.** Pointing `MethodDeclarationNode` at the
override itself made `MethodDeclarationId` self-referential. OPC 10000-4
lets a caller invoke a method with the NodeId of its declaration in the
ObjectType *or any supertype*, and `MethodState.MethodDeclarationId` is
what the server matches on, so `Call` stopped resolving and returned
`Bad_MethodInvalid` for every alarm subtype. An override that genuinely
changes its signature still becomes its own declaration.
- **`StateNumbers` / `TransitionNumbers` were emitted empty for every
NodeSet-sourced model**, because such children arrive as
`VariableDesign` rather than `PropertyDesign` and were filtered out. Any
companion model built from a NodeSet was writing `StateNumber = 0` on
the wire — a Part 16 defect well beyond the models in this stack. The
ModelDesign path is unchanged.
- **Referenced models can contribute fluent accessors**, carried by a
model dependency payload so a downstream model can extend an upstream
one.

## Notes for the reviewer

- `tests/Opc.Ua.SourceGeneration.Tests` is left at its `master` content
here. Its new vendor-subtype test uses the Robotics NodeSet as its
fixture, so it can only land with the Robotics change at the top of the
stack (5/5).
- `PumpInstanceNodeIdRegressionTests` gains six
`InputArguments`/`OutputArguments` entries for argument-less standard
condition methods. Typed method signatures materialise those properties
from the `ConditionType` declaration even where the method takes none.
That is a real deviation, pinned rather than hidden so it stays visible;
it is worth a follow-up.

## Validation

`dotnet build UA.slnx` — 0 errors.

| Suite | Result |
| --- | --- |
| Opc.Ua.SourceGeneration.Core.Tests | 3739 / 0 |
| Opc.Ua.SourceGeneration.Tests | 81 / 0 |
| Opc.Ua.Di.Tests | 304 / 0 |
| Opc.Ua.History.Tests | 506 / 0 |
| Opc.Ua.Gds.Tests | 1095 / 0 |

History and GDS are listed because they are what the method-declaration
fix above is load-bearing for: without it they fail with 10 and 370
errors respectively.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
marcschier added a commit that referenced this pull request Aug 1, 2026
…provider (#4125)

**Stack 3/5** — base #4124. Part of the #4096 split; see #4096 for the
overall picture.

> Review after #4123 and #4124.

## What this is

The OPC 10000-210 Relative Spatial Location and OPC 10000-211 Global
Positioning companion models, with their server and client libraries,
plus the ISA-95 migration onto the shared geolocation seam.

## Why it is one change

A deployment writes **one** provider implementation and both OPC
10000-211 and OPC 10030 are served from it. The specifications disagree
on representation, not on content:

- GPOS publishes the structured `GlobalLocationDataType` and validates
the EPSG code against the configured CRS.
- ISA-95 publishes the same sample as the `String` array that OPC 10030
§8.3.2.2 requires.

Splitting them would mean two provider interfaces for one physical
sensor, which is exactly what this avoids. The seam itself lives in
`Opc.Ua.Server` and landed in 1/5.

## Contents

- `Opc.Ua.Positioning`, `.Server` and `.Client`.
- `Opc.Ua.ISA95.Server` moves onto `IGeoLocationProvider`; the three
ISA-95-specific provider files are removed because the shared seam and
its in-memory implementation replace them.
- **Fixes a conformance gap**: the ISA-95 binder wrote a scalar where
the type declares `ValueRank` OneOrMoreDimensions. It now writes
`String[]`.
- **Fixes a liveness bug**: a provider that cannot push is polled at the
Variable's `MinimumSamplingInterval` rather than installed once, so a
non-pushing provider no longer serves its start-up sample with `Good`
status forever.
- `samples/MinimalIsa95Server`.

## Validation

`dotnet build UA.slnx` — 0 errors.

| Suite | Result |
| --- | --- |
| Opc.Ua.Positioning.Tests | 69 / 0 |
| Opc.Ua.ISA95.Tests | 136 / 0 |
| Opc.Ua.Di.Tests | 304 / 0 |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
marcschier added a commit that referenced this pull request Aug 1, 2026
…nector (#4126)

**Stack 4/5** — base #4125. Part of the #4096 split; see #4096 for the
overall picture.

> Review after #4123, #4124 and #4125.

## What this is

The generic OPC UA to OpenUSD binding, scene materialization, and the
connector that drives a live stage from a running server.

## Contents

**Binding (Part 1)** — `Opc.Ua.OpenUsd` and `Opc.Ua.OpenUsd.Server`. A
server describes how its Variables drive USD prims, and serves the USD
layers it refers to as Part 5 files with a content digest.

**Scene materialization (Part 2)** — `Opc.Ua.OpenUsdScene` and
`Opc.Ua.OpenUsdScene.Server`, including the USDA writer.
- **Escaping fix**: string values, composition-arc references, variant
set names and selections, `doc` and prim names were all emitted
unescaped, so a value carrying a quote or a newline could author its own
layer syntax. Every emit site now goes through the escapers. The tests
assert the right property — not that the payload is absent (it
legitimately survives inside the escaped literal) but that no payload
becomes its own line of layer syntax.

**Client** — `Opc.Ua.OpenUsd.Client` streams live values into an
`IUsdSink`. `CompositeUsdSink` fans out to several sinks; the file sink
interpolates between server samples so motion is continuous rather than
stepped at the publishing interval.

**Connector** — `tools/Opc.Ua.OpenUsd.Connector` composes a stage from a
live server. The optional in-process viewport lives in a separate
assembly probed at run time, so the connector still runs — and says why
— when it is absent.
- **Fix**: the asset cache and PKI root moved off the shared temp
directory to `LocalApplicationData`.

**Sample** — `PumpDeviceIntegrationServer` gains an OpenUSD
representation of the pump.

## Notes for the reviewer

- The viewport assembly is net10.0-only. It is what exercises the
`Directory.Build.targets` change from 1/5: without it, the `net8.0` leg
of the per-TFM CI build fails with `CS0433` because the
`System.Threading.Lock` polyfill collides with the real type.
- `UsdAssetDelivery` hashes through `SHA256.Create` rather than the
static `HashData`, which does not exist on the net472/net48 targets this
test project also builds for.

## Validation

`dotnet build UA.slnx` — 0 errors, 0 warnings.

| Suite | Result |
| --- | --- |
| Opc.Ua.OpenUsd.Tests | 621 / 0 |
| Opc.Ua.Di.Tests | 321 / 0 |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
marcschier added a commit that referenced this pull request Aug 1, 2026
#4127)

**Stack 5/5** — base #4126. Part of the #4096 split; see #4096 for the
overall picture. Closes the API work for #3827.

> Review after #4123, #4124, #4125 and #4126. This branch is
content-identical to the original #4096 branch, so merging the stack
lands exactly what #4096 contained.

## The problem this solves

OPC 40010 1.02 defines a rich robot **topology** but **no motion verbs
at all** — its actuation surface is the SystemOperation and TaskControl
state machines plus a Controller `Programs` directory. Meanwhile
[URML](https://github.com/URML-MARS/URML) names OPC UA Robotics as its
canonical non-ROS substrate and asks for `MoveTo` / `Grasp` / `Release`
methods the companion spec does not define, so today it needs a
hand-maintained per-deployment NodeId mapping file.

This closes that gap in two clearly separated tiers, and never presents
the second as standard.

## Contents

**Server** — stock `RoboticsNodeManager`, ordered
`IRoboticsModelProvider` composition, `IRoboticsConfigurator`
code-behind, and validated fluent topology builders for systems,
controllers, motion devices, axes, power trains, motors, gears, drives,
safety states, software, task controls and task modules, with the
correct OPC 40010 semantic references.

**Standard operations** — `AddSystemOperation` and
`AddTaskControlOperation` drive the Part 16 state machines. An illegal
cause is rejected with `BadInvalidState` before the handler runs; a
handler returning a bad `ServiceResult` does not move the machine; and a
transition already in flight rejects a concurrent one rather than
letting both commit.

**Programs** — binds the optional `FileDirectoryType` through the
reusable `IFileDirectoryBinder` added in 1/5.

**Operation conventions** — opt-in and explicitly non-normative `MoveTo`
/ `MoveJ` / `MoveL` / `Grasp` / `Release` / `PickFrom` / `PlaceAt` /
`SwapTool` / `SetOutput` / `CallProgram`, created **only** in an
application-owned namespace — the OPC UA, DI, IA and Robotics namespaces
are rejected — plus a generic `AddOperation<TRequest, TResponse>`
extension point.

**Client** — `RoboticsClient` extends the DI client, is subtype aware
throughout, and provides typed topology snapshots, the standard
operation and task-control clients, `ProgramsAsync`, streaming
observation and a DI factory. Verbs resolve by BrowseName, so no NodeId
mapping file is needed.

**Sample** — `MinimalRobotServer` composes five models (Robotics, IA,
DI, OpenUSD, RSL/GPOS) purely through `IRoboticsModelProvider`
contributions, which is the real proof that the stock hosting is
sufficient. It follows the published KUKA KR 16-2 kinematics and axis
limits with an eased pick-and-place cycle.

Also restores the vendor-subtype scanner test held back from 2/5, which
uses the Robotics NodeSet as its fixture.

## Defects found and fixed along the way

- **`ControllerType.CurrentUser`** is Mandatory in OPC 40010 but was
neither built nor validated. Now has a builder and a validation error.
- **`MotionDeviceType.TaskControlReference`** was absent because it must
target a `TaskControlOperationType` instance that did not exist. Now
wired.

## Validation

`dotnet build UA.slnx` — 0 errors, 0 warnings.

| Suite | Result |
| --- | --- |
| Opc.Ua.Robotics.Tests | 103 / 0 |
| Opc.Ua.OpenUsd.Tests | 635 / 0 |
| Opc.Ua.Di.Tests | 342 / 0 |
| Opc.Ua.SourceGeneration.Tests | 94 / 0 |
| Opc.Ua.Aot.Tests (NativeAOT) | 120 / 0 |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
@marcschier

Copy link
Copy Markdown
Collaborator Author

Superseded — all of this landed on master as the five stacked PRs:

Verified against master before closing, rather than assuming:

Nothing outstanding, so closing this tracking PR and deleting the branch.
Follow-ups from review are tracked separately in #4137 and #4151.

@marcschier marcschier closed this Aug 1, 2026
@marcschier
marcschier deleted the marcschier/robotics-3827-api branch August 1, 2026 06:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Robot companion spec libraries to support URML (open robot intent language)

1 participant