Add OPC UA Robotics companion SDK (tracking: split into #4123-#4127) - #4096
Closed
marcschier wants to merge 55 commits into
Closed
Add OPC UA Robotics companion SDK (tracking: split into #4123-#4127)#4096marcschier wants to merge 55 commits into
marcschier wants to merge 55 commits into
Conversation
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.
… namespace safety
…e+owned-session close, asset size caps, serialized recompose
…, ITelemetryContext + source-generated logging
…igest struct assertions
…ory, DI, Robotics client (56 tests)
…I-overload tests, add OpenUsd docs
…or in runner (IAsyncDisposable)
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
commented
Jul 25, 2026
…-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.
marcschier
commented
Jul 26, 2026
marcschier
commented
Jul 26, 2026
marcschier
commented
Jul 29, 2026
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
This was referenced Jul 30, 2026
… 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
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The stack
Merge in order; each targets the one above it.
IFileDirectoryBinder, theIGeoLocationProviderseam,FiniteStateMachineDispatcher, NodeId-factory and JSON decoder fixes, build-matrix restrictionStateNumbersfixesMinimalIsa95ServerPumpDeviceIntegrationServerMinimalRobotServer. Closes the API work for #3827No 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
MinimalRobotServercomposes 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:
FileDirectoryType-backedProgramsdirectory, opt-in non-normative motion verbs, and a subtype-aware client that resolves verbs by BrowseName so no NodeId mapping file is needed.Defects found and fixed
ControllerType.CurrentUseris Mandatory in OPC 40010 but was neither built nor validated.MotionDeviceType.TaskControlReferencewas absent because it must target aTaskControlOperationTypeinstance that did not exist.StateNumbers/TransitionNumbersemitted empty for every NodeSet-sourced model — a Part 16 defect well beyond Robotics.NodeIdFactory, so a counting factory silently burned identifiers and a tracking one leaked them.Callwith a supertype's MethodId returnedBad_MethodInvalid— 10 Alarms and Conditions and 370 GDS conformance failures.GeoSpatialLocationwas written as a scalar where OPC 10030 §8.3.2.2 declares OneOrMoreDimensions.Goodstatus forever.OnReadgetter 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