Files
UA-.NETStandard/samples/Robotics/MinimalRobotServer/README.md
5899354441 Clarify documentation, migration guidance, and SDK selection (#4823)
# Description

Remove stock phrasing and correct misleading or inconsistent technical
documentation. The initial two passes covered 367 Markdown files,
followed by an interactive review of 169 editorial flags. The U5
interpretation originally deferred in that review has since received the
upstream Mantis answer recorded by #4843; remaining server/spec
follow-ups stay explicit in the CTT log.

- Correct package responsibilities, migration steps, API names,
ownership, protocol descriptions and links. Keep bundled migration
guides synchronized. Preserve historical conclusions as historical
records with separate re-check notes.
- Provide at least two rendered TOC levels throughout `docs/`. Convert
appropriate architecture figures to Mermaid without changing code
examples, transcripts or historical quotations.
- Address the October 19 feedback with concrete setup/migration
instructions, corrected certificate paths and PubSub names, shorter
repeated caveats, consistent XML comments, and a full review of the
Concepts guide. Retain #4839's API corrections.
- Integrate #4841's runtime/performance guidance and review
incoming/recent prose. Preserve dated measurements rather than claiming
new benchmark runs; clarify allocation ratios, startup-only processor
settings, JIT warm-up and the benchmark commit's scope. Missing raw
measurement artifacts and repeat ranges remain tracked by #4842.
- Merge master through `e8c5d800a` in `32c6394b7`. Resolve the CTT log
conflict using the updated Mantis states and OPC answers, retaining
historical framing and re-checks for C28/C32/C43.
- Address the latest naming/index feedback in `adb5bf693`: rename the
guides to `docs/WotCon.md` and `docs/WotNodeSetConversion.md`; move the
provider and DataType-declaration contracts into the conversion guide
and remove `docs/WoT.md`. Rename `samples/DI` to `samples/Di` and
`samples/Isa95` to `samples/ISA95`. Add content-index READMEs to Di,
FluentApi, ISA95, Lds and PubSub; update solution entries, project
references, Docker/CI paths, test fixture paths and documentation links
with exact casing.

**WoT guide scope.** This branch retains the detailed conversion guide
that #4758 had replaced with an anchor stub. Conversion limits, verified
document sets, preservation rules and provider ownership now live
together in `WotNodeSetConversion.md`; binding/runtime guidance remains
in `WotBindings.md` and `WotCon.md`. The design-rationale/test-ledger
prose was shortened, the oversized argument-table cell moved into its
section, and existing anchors preserved.

**SDK policy is a separate, explicit user-directed change.**
`global.json` sets a stable minimum of `10.0.400`, `rollForward:
latestMajor`, and `allowPrerelease: false`. Newer major SDKs are
permitted; CI's explicit SDK pins are unchanged. README, GettingStarted,
DeveloperGuide and Recycling instructions reflect the possible analyzer
differences. The image-contract regression fix accepts compatible stable
SDK images instead of requiring exact equality with the floor.

The latest four rename/index threads are resolved after publication.
Three older threads remain open: universal obsolete-removal timing,
unconditional nonce-strength wording, and a blanket Vision NativeAOT
claim. Existing implementation/evidence constraints are explained in
their inline replies. The two-level TOCs remain as requested.

## Validation

- `git diff --check` and staged diff checks: passed. The untouched
upstream AutoID NodeSet retains its provenance bytes, including two
existing license-header trailing spaces.
- `.\.azurepipelines\validate-migration-plugin.ps1`: passed; 15 bundled
migration documents and manifests synchronized.
- Documentation/path validation: **101** documents retain two rendered
TOC levels (**2,228** entries), with idempotent formatting. **52** new
relative links and their anchors passed exact-case checks. No tracked
references to the old guide/sample paths remain. Both relocated
provider-contract sections are preserved in the destination.
- `dotnet sln UA.slnx list`: passed; shows the renamed Di/ISA95 projects
and the existing FluentApi/Lds/PubSub projects. Updated solution and
pump ProjectReference paths also passed exact-case existence checks.
- Latest focused commands, run from the repository root:

```powershell
# Passed: 61 channel send-order and reconnect tests.
dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~UaSCBinaryChannelSendOrderTests|FullyQualifiedName~TcpReconnectKeyAgreementTests' --verbosity minimal

# Passed: 5 pump datasheet tests; builds the renamed pump sample.
dotnet test tests\Opc.Ua.Di.Tests\Opc.Ua.Di.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~PumpDatasheetConformanceTests' --verbosity minimal

# Passed: 4 repository NodeSet path/round-trip tests, including the moved pump models.
dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~RepositorySourceSurvivesSerializedLinkedPartitionsAsync' --verbosity minimal

# Passed: build, zero warnings/errors.
dotnet build samples\ISA95\MinimalIsa95Server\MinimalIsa95Server.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal

# Passed: dependent sample-test project build, zero warnings/errors.
dotnet build tests\Opc.Ua.WotCon.Samples.Tests\Opc.Ua.WotCon.Samples.Tests.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal
```

Earlier focused integration checks passed 2,620 tests across AutoID,
event queues/scheduling, source generation, MCP/package profiles and
WoT. Both reviewed Mermaid diagrams rendered locally without network
access. These checks are not a full solution run.

The full solution has **not** been rerun for this review. Earlier
`dotnet test UA.slnx -p:CustomTestTarget=net10.0 --verbosity quiet` did
not pass: three AOT projects rejected VSTest and 20 Vision tests failed
during graphics initialization with `OutOfMemoryException`. Remaining
long-running tests were stopped at the user's request. A focused Vision
rerun passed 558 tests and reproduced those 20 failures. The earlier
`dotnet test UA.slnx -p:CustomTestTarget=net48 --verbosity quiet` was
blocked by the former SDK pin and was not retried after the policy
change. The user approved proceeding with those limitations documented;
current remote CI is not claimed green.

## Related Issues

- Follow-up #4842 tracks the requested 2.0.2+ benchmark/tuning and
diagnostic-tool documentation refresh. #4841 supplies documentation and
earlier results, not all newly requested measurements.
- #4758 explains the conversion guide's earlier consolidation; the
current provider contracts are now consolidated into
`WotNodeSetConversion.md`.
- #4839 supplies the reviewed API corrections; #4843 supplies the
updated CTT issue record.
- The original prose cleanup and SDK policy were user-requested without
a source issue. No new runtime architecture is introduced by these
documentation edits.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: Roman Ettlinger <romanett98@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:10:55 +02:00

16 KiB
Raw Permalink Blame History

MinimalRobotServer

A minimal, self-contained .NET console OPC UA server that demonstrates the OPC 40010 Robotics, OPC 10000-210 Relative Spatial Location, and OPC 10000-211 Global Positioning companion specifications bound to OpenUSD through the draft OpenUSD binding companion model, so a generic connector renders a robot cell live with no robot-specific code. It is built on the Opc.Ua.Robotics and Opc.Ua.OpenUsd SDK libraries. See docs/Robotics.md for the Robotics developer guide and docs/OpenUsd.md for the OpenUSD binding.

It is the Robotics counterpart of PumpDeviceIntegrationServer and is validated end-to-end by RobotOpenUsdE2eTests (in tests/Opc.Ua.Di.Tests).

What it exposes

A MotionDeviceSystem "RobotCell" (prim /Cell) composed recursively of:

  • Two 6-axis articulated robots (MotionDeviceType R1, R2 → /Cell/Robots/R1, /Cell/Robots/R2), aggregated 1..n with a Many / Reference <Component> binding (Reference — not Instance — so each robot articulates independently).
  • Six axes per robot (AxisType A1..A6), aggregated with a nested Many / Child <Component> binding. Each Axis' ParameterSet/ActualPosition (degrees) drives one joint xformOp:rotate{Z|Y|X} on the (pre-authored) robot.usda kinematic chain — the live articulation (RenderTargetKind = Rotation). The axis limits are those of the reference robot (see below). The axis values come from an arm solver, so the arm reaches the slot it is working rather than replaying a canned pose list.
  • A cell EmergencyStop safety state driving a beacon and per-robot warning halo visibility (UaAlarmToUsd, Visibility).
  • An opt-in SpeedOverride command on R1 (UsdToUaCommand, fail-closed). R2's speed override is read-only and cannot redirect a write into R1.
  • A gripper tool on each robot's flange, plus one mounted on R1 at runtime (One / Reference, Dynamic = true) via a model-change event. R1 carries a NodeVersion property so that mounting it is reportable at all — see Dynamic components need a NodeVersion.
  • A CellTwin folder under Objects carrying the state that is neither a device nor an axis: a ThreeDCartesianCoordinates position and a ThreeDOrientation heading per workpiece, driving /Cell/Parts/PartNN (Translation and Rotation), and a jaw position per robot driving …/Flange/Tool/Jaw{Upper|Lower}. Without these the choreography is invisible — the robots mime a transfer while the blocks stay wherever the asset authored them.
  • One RSL spatial-object list with a world frame, R1/R2 SpatialObject AddIns, each robot's PositionFrame, and R1's ToolFlange AttachPoint.
  • One GPOS Zone with ground-control points and one live GlobalLocation per robot. Both poses come from the shared CellChoreographer, so the robots agree on where each other is - see The transfer cycle.

All 15 representations (1 system + 2 robots + 12 axes) are discoverable through the well-known Server/OpenUSD/Representations registry.

The transfer cycle

R1 collects a part from the western station (WorkTableA) and delivers it to the eastern one (WorkTableB); R2 collects from the east and returns it to the west. Three parts circulate, so both robots usually have work.

Keeping them apart

The cell is divided into reservable zones:

Zone Extent Occupancy
EndZoneA / EndZoneB |x| > 1.3 m one robot — a deployed arm needs the whole end
CorridorEastbound |x| ≤ 1.3 m, northern lane (y = 0.35) one robot
CorridorWestbound |x| ≤ 1.3 m, southern lane (y = −1.15) one robot
DockA / DockB around each charging dock (y = −2.0) one robot

A robot reserves the zone immediately ahead before entering it and releases the one behind once it is clear — block signalling, not a global schedule. Three rules follow:

  • Two robots travelling opposite ways pass, because they are in different lanes 1.5 m apart. Two travelling the same way queue instead of overtaking.
  • An arm may only leave its transport envelope inside an end zone, which the robot holds exclusively. This is the rule that fixes the original defect.
  • A robot with no work parks on its dock, releasing the end zone so the other robot can deliver into the station it serves. Without this the cell deadlocks on the first handover — an idle robot standing by its table blocks the delivery it is waiting for.

The docks sit in a southern layby rather than beside the stations, because a robot parked next to a station is close enough to foul a robot turning into it.

What else is simulated

  • Trapezoidal travel — accelerate, cruise, brake into the stop, with the heading following the path.
  • Speed and separation — travel speed drops near the other robot, and the cell emergency stop now halts motion rather than only blinking the beacon.
  • Battery and charging — charge drains with distance; below the threshold the robot finishes its delivery, docks, charges and resumes.
  • Grip faults — a seeded slip occasionally drops a part; the robot re-picks it off the floor. Deterministic, so the recovery path is testable.
  • KPIs — parts moved, cycle count, last and average cycle time, utilisation, fault count.

The invariants are asserted rather than assumed: CellChoreographyTests runs ten minutes of simulated time and checks on every 50 ms step that the oriented footprints never overlap, that a zone is never doubly occupied, that arms only deploy in an end zone, that parts are conserved, and that a carried part sits exactly on the tool centre point.

Where the part actually is

A carried part's pose is computed by forward kinematics from the same six axis values the server publishes (RobotKinematics), not from the script that produced them. This keeps it welded to the gripper: if the arm and the part were computed independently they would drift apart. RobotArmSolver is checked against that same forward kinematics, so the arm provably reaches each slot to within a micrometre.

It is computed from the platform pose that was last published, not the one the simulation currently holds. The platform and the workpiece leave the server as separate bindings, and publishing them from loops running at different rates floated the part a quarter of a metre out in front of the jaws while the robot drove — three part widths, and the first thing anyone notices in a viewport. RobotOpenUsdE2eTests rebuilds the tool centre point from the values authored into the scene and asserts a carried part sits on it.

The buffers are worked first-in-first-out. Collecting the lowest free slot instead starves any part that never lands in it: with two parts seeded on WorkTableA, the second sat untouched for the life of the process while the robots shuttled the other one past it.

Editing a live-bound prim

Every prim the server drives declares exactly one matrix4d xformOp:transform and orders only that op — the parts and the gripper jaws included. A viewer composes the translate and rotate ops it receives into that single matrix, and an op order declared in a referenced asset sits in a weaker layer than the one a connector edits, so it could not be cleared from there. RobotAssetContractTests asserts this for every bound prim.

The gripper jaws are one Xform each, owning their carrier and their finger. Driving the carrier alone would slide it out from under the finger it is bolted to.

Dynamic components need a NodeVersion

Part 5 §9.32.2 allows only a node that carries a NodeVersion property to trigger a ModelChangeEvent, and AsyncCustomNodeManager drops entries for nodes that lack one. A Dynamic component binding is reconciled from those events, so the node whose references change — here the robot the tool is mounted on — has to expose NodeVersion, via EnableModelChangeTracking. Without it the mount is filtered out and no client is ever told the gripper appeared.

The failure was asymmetric. Removal kept reporting, because a deleted node is no longer in the manager's index and so takes the "not mine, pass it through" branch of the filter; only addition was dropped. A connector that happened to start while the tool was mounted therefore looked correct and could still deactivate it, while one that started in the six-second gap never composed the tool at all and never recovered.

DynamicToolIsComposedAsync asserts the transition — the prim observed both active and inactive across more than one full cycle — rather than a momentary state, so it cannot pass on a connector that only ever hears about one direction.

The opening view

The cell authors /Cell/OverviewCamera and a connector opens on it, because framing the bounds of an enclosed scene automatically puts the eye inside the fence, looking at whichever robot happens to be nearest. The first camera in the served root layer wins, so the establishing shot is authored before /Cell/TopDownCamera; --camera overrides the choice outright. RobotAssetContractTests asserts the ordering.

Ideas for more realism

Natural extensions that are deliberately not implemented, roughly in order of value:

  • Speed-and-separation monitoring to ISO 10218 / TS 15066, with a modelled protective field, replacing the simple proximity speed scale.
  • Sensor cones on the existing ScannerFront / ScannerRear prims, with detections driving the slowdown instead of omniscient positions.
  • A tool changer — dock the gripper and pick up a different end-effector, which the dynamic-component binding already demonstrates the mechanics for.
  • An inspection station that rejects a part, giving the routing a real branch.
  • Conveyor infeed and outfeed instead of a fixed part population.
  • Operator gate interlock — opening the front gate stops the cell.
  • Joint torque and current telemetry, plus thermal drift, so axes carry more than position.
  • Maintenance counters (distance travelled, grip cycles) feeding a service alarm.
  • Time-sampled pose history so a viewer can scrub the last cycle rather than only following live values.
  • Traffic priority — today a blocked robot simply waits; a real fleet manager would weigh deadlines and re-route.

The USD assets

Assets/robot.usda, Assets/tool.usda and Assets/Cell.usda are embedded in the server and served over Part 5 FileType with SHA-256 digests, so a connector can fetch the whole closure and render the twin without any external asset resolver.

Reference robot — KUKA KR 16-2

robot.usda reproduces the published kinematics of a KUKA KR 16-2, taken from the ROS-Industrial URDF (ros-industrial/kuka_experimental, kuka_kr16_support):

Joint Offset from the previous axis Rotate op Limit
A1 675 mm up from the mounting face xformOp:rotateZ ±185°
A2 260 mm forward xformOp:rotateY −155°…+35°
A3 680 mm along the upper arm xformOp:rotateY −130°…+154°
A4 670 mm along the forearm, −35 mm lateral xformOp:rotateX ±350°
A5 coincident with A4 xformOp:rotateY ±130°
A6 coincident with A4 xformOp:rotateX ±350°
Flange 158 mm past the wrist centre — —

The arm's published reach is 1611 mm; the cell's 7.2 × 4.8 m floor is sized around it. A4, A5 and A6 are coincident, which is the real spherical wrist. The livery is KUKA orange over dark cast housings, and the cell has guarding at x = ±3.4 m, y = ±2.2 m, plus a controller cabinet, two transfer stations, two charging docks and a stack light.

Mobile platform

Each arm rides on an omnidirectional AGV (/Robot/MobileBase, 1.10 × 0.72 m, 365 mm deck) in the style of a KUKA KMP: orange chassis, dark skirt, four wheels, diagonally opposed safety scanners, bumpers and status strips. The RSL scenario drives each robot's spatial location across the cell, so the arm has to be carried rather than bolted to the floor.

Platform clearance is not enough on its own. An earlier version of this sample ran the two robots on independent figure-eight paths and claimed the footprints cleared. They did — by 1.2 m at closest approach — but the arms reach 1.611 m, so they swept straight through each other. Clearance has to be argued about the deployed arm, not the chassis, which is why the cell is now zoned and the arm must be stowed to travel.

To floor-mount the arm instead, delete /Robot/MobileBase and clear the xformOp:translate on /Robot/Base; nothing else depends on it.

Editing the assets

The prim paths and rotate ops above are the binding contract: RobotCell.cs addresses them by name, and the connector writes each joint rotate op as a scalar double. Link offsets, geometry, materials and lighting are all free to change; the paths, op names, xformOpOrder entries, /Robot/Base/.../Flange, /Robot/Warning, /Cell/SafetyBeacon, /Cell/Parts/PartNN, /Gripper/Jaw{Upper|Lower}, /Cell.inputs:speedOverride and the R1/R2 mount attributes are not.

RobotAssetContractTests (in tests/Opc.Ua.OpenUsd.Tests) parses the shipped assets and asserts that contract, so an edit that breaks it fails the build instead of silently freezing the twin. Note that the .usda files under tests/Opc.Ua.OpenUsd.Tests/Assets are a frozen reader/materializer fixture and are deliberately not kept in sync with these assets.

Author with real USDA syntax: the managed UsdaReader is more forgiving than OpenUSD itself, so a layer that parses in the tests can still fail to load in a viewer. The end-to-end run below is the authoritative check.

Design note — stock Robotics hosting

The server uses the stock Opc.Ua.Robotics.Server hosting pipeline:

  • AddRobotics() registers the sealed stock RoboticsNodeManager, DI/IA/Robotics model provider, and the shared Robotics configurator pipeline.
  • AddRoboticsModel<OpenUsdModelProvider>(), AddRoboticsModel<RslModelProvider>(), and AddRoboticsModel<GposModelProvider>() contribute the additional compiled OpenUSD, RSL, and GPOS companion models to the same manager.
  • ConfigureRobotics<RobotCell>() builds the cell with the validated fluent topology builders (AddMotionDeviceSystemAsync, AddMotionDevice, AddAxis, safety, controller, task-control, and operation builders).
  • AddPositioningFor<RoboticsNodeManager>() and ConfigurePositioningFor<RoboticsNodeManager>(...) add the RSL/GPOS spatial objects, frames, attach point, zone, and live global-location bindings after the Robotics topology exists.

The OpenUSD facility still lives under the standard Server/OpenUSD entry point, but it is now authored by the RobotCell configurator against the stock manager instead of by subclassing a custom node manager.

Run it

dotnet run --project samples/Robotics/MinimalRobotServer -- --host localhost --port 62830

Then drive it with the same generic connector used for pumps (Opc.Ua.OpenUsd.Connector) to author a live USD override layer:

dotnet run --project tools/Opc.Ua.OpenUsd.Connector -- \
    --server opc.tcp://localhost:62830/MinimalRobotServer \
    --out <path>/live.usda --insecure --seconds 10

Compose live.usda over the base Cell.usda (see the example stage.usda) and open it in usdview / NVIDIA Omniverse to see the two arms articulate live. The example USD assets, descriptor, writer, and a step-by-step guide live alongside the OpenUSD binding specification, under its examples/robotics/ directory.

To watch the cell animate without leaving the connector, install the optional Opc.Ua.OpenUsd.Connector.Viewer assembly beside it and add --view:

dotnet run --project tools/Opc.Ua.OpenUsd.Connector -- \
    --server opc.tcp://localhost:62830/MinimalRobotServer --insecure --view

That fetches this server's served asset closure, composes stage.usda, opens a viewport, and streams the same subscribed values into both the override layer and the rendered stage, so the joints move on screen as the simulation runs. See docs/OpenUsd.md.

The robot motion is configured through RobotMobilityOptions. RSL Position/Orientation values author xformOp:translate and xformOp:rotateXYZ; GPOS longitude, latitude, and elevation author the inputs:longitude, inputs:latitude, and inputs:elevation attributes. See docs/Positioning.md.