Files
Marc Schier 7e25e1a3d7 Extract remaining shared companion, rendering and packaging changes (#4806)
# Description

The shared AI deployment contracts and instance registration; DI
software-upload outcomes, digest handling and simulator; bounded
Positioning/Pumps reads; PubSub raw-data/matrix and security-provider
handling; Vision read-quality contracts; xRegistry resource writes and
instance projection; OpenUSD authoring, embedded viewport and
retained-frame capture APIs; standalone sample CLI controls; and the
generic Debug/RID package-policy and fork/Dependabot coverage-comment
fixes. The Linux native prerequisite step is retained for **Vision tests
only**.

## Related Issues

- Related tracking issue:
[#4461](https://github.com/OPCFoundation/UA-.NETStandard/issues/4461),
without automatic closure.
- Source work:
[#4462](https://github.com/OPCFoundation/UA-.NETStandard/pull/4462),
which remains intact for the coordinator's later integration.
- Immediate stacked predecessor:
[#4804](https://github.com/OPCFoundation/UA-.NETStandard/pull/4804).

## 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.
2026-10-17 07:52:02 +02:00

302 lines
12 KiB
Markdown

# OPC UA Relative Spatial Location and Global Positioning
The Positioning libraries implement two OPC UA companion specifications:
- Relative Spatial Location (RSL), defined by
[OPC 10000-210](https://reference.opcfoundation.org/specs/OPC-10000-210).
- Global Positioning (GPOS), defined by
[OPC 10000-211](https://reference.opcfoundation.org/specs/OPC-10000-211).
The libraries use the released RSL 1.00.1 and GPOS 1.0.0 NodeSets. GPOS
depends on RSL.
## Contents
- [Packages](#packages)
- [Minimal standalone server](#minimal-standalone-server)
- [Server authoring](#server-authoring)
- [Client](#client)
- [Transform conventions](#transform-conventions)
- [Robot and OpenUSD sample](#robot-and-openusd-sample)
## Packages
| Package | Purpose |
|---|---|
| `OPCFoundation.NetStandard.Opc.Ua.Positioning` | Source-generated RSL and GPOS models plus frame, WGS84, ENU, and ground-control-point transformations. |
| `OPCFoundation.NetStandard.Opc.Ua.Positioning.Server` | Standalone/composed node-manager hosting, address-space builders, providers, lifecycle, validation, and logging. |
| `OPCFoundation.NetStandard.Opc.Ua.Positioning.Client` | Continuation-safe discovery, typed reads and streams, frame-chain resolution, and Zone transforms. |
Generated model types remain in the specification namespaces:
- `Opc.Ua.Rsl`
- `Opc.Ua.Gpos`
The base package exposes generated NodeIds, NodeStates, encodeables, model
loaders, and generated ObjectType clients. The hand-written APIs compose those
generated basics instead of replacing or inheriting from them.
## Minimal standalone server
`AddPositioningServer` owns an RSL/GPOS node manager. Configure the address
space after the generated models are loaded:
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Opc.Ua.Positioning.Server;
using Opc.Ua.Positioning.Server.Hosting;
HostApplicationBuilder host = Host.CreateApplicationBuilder(args);
IPositioningServerBuilder positioning = host.Services
.AddOpcUa()
.AddServer(options =>
{
options.ApplicationName = "PositioningServer";
options.EndpointUrls.Add(
"opc.tcp://localhost:4840/PositioningServer");
})
.AddPositioningServer();
positioning
.AddGeoLocationProvider<MyGpsProvider>()
.AddRelativeSpatialLocationProvider<MyRelativeLocationProvider>()
.ConfigurePositioningFor<PositioningNodeManager>(async context =>
{
PositioningAddressSpaceBuilder model = context.AddressSpace;
// Create a SpatialObjectsList, SpatialObject AddIns and concrete
// Cartesian frames, then register each completed subtree.
// Create Zones and attach GlobalPosition or GlobalLocation Variables
// in the same callback.
await ValueTask.CompletedTask;
});
await host.Build().RunAsync();
```
For a server that already owns the companion address space, keep that manager
as the single namespace owner:
```csharp
IPositioningServerBuilder positioning = serverBuilder
.AddNodeManager<MyNodeManagerFactory>()
.AddPositioningFor<MyNodeManager>();
positioning.ConfigurePositioningFor<MyNodeManager>(
context => ((MyNodeManager)context.Manager)
.ConfigurePositioningAsync(context));
```
The owning composite manager must load the RSL model before GPOS and advertise
both namespace URIs.
## Server authoring
`PositioningAddressSpaceBuilder` creates instances through the generated
factories and supports:
- `SpatialObjectsListType` AddIns below the standardized
`RelativeSpatialLocations` entry point.
- `SpatialObjectType` AddIns attached to existing objects through `HasAddIn`.
- Cartesian world, position, attach-point, internal, and alternative frames.
- GPOS `ZoneType` instances defined by ground control points or a
position/radius.
- `GlobalPositionType` and `GlobalLocationType` Variables attached to tracked
objects.
- Coherent aggregate/component updates, optional-field status, `NodeVersion`,
and model-change notifications.
Provider bindings publish an initial value and then consume a cancellation-aware
async stream:
```csharp
PositioningProviderSubscription subscription =
await model.BindGlobalLocationAsync(
globalLocation,
provider,
sourceId,
cancellationToken: ct);
```
Dispose the subscription asynchronously during server shutdown. Provider
failures retain the last value, set its status to `BadCommunicationError`, log
the failure through `ITelemetryContext`, and fault `Completion`.
`IGeoLocationProvider` is intentionally technology-neutral, and lives in
`Opc.Ua.Server` (namespace `Opc.Ua`) rather than in a companion-model assembly.
Global Positioning System (GPS) and World Geodetic System 1984 (WGS84)
coordinates are built-in use cases. Real-time location systems (RTLS),
ultra-wideband (UWB), radio-frequency identification (RFID), local
floor-plan coordinates, and other tracking systems can use the same contract.
An RSL provider
is also useful when a robot controller, metrology system, or kinematic service
is authoritative for a relative frame rather than a global coordinate.
Because the contract is shared, **one provider implementation serves every
model that publishes location**. The same instance can back GPOS
`GlobalLocation` Variables here and OPC 10030 (ISA-95) `GeoSpatialLocationType`
Variables — see [ISA-95](ISA95.md#geospatiallocationtype-provider-seam).
A sample can include:
- `GeoPosition` with latitude, longitude, optional height, accuracy, floor,
and EPSG code
- `GeoOrientation`
- Text labels
```csharp
public sealed class MyGpsProvider : IGeoLocationProvider
{
public bool SupportsPush => true;
public ValueTask<GeoLocationSample> ReadAsync(
string sourceId,
CancellationToken ct = default)
{
return new ValueTask<GeoLocationSample>(
GeoLocationSample.Good(
new GeoPosition(47.3769, 8.5417, 408.0, EpsgCode: 4326)));
}
public async IAsyncEnumerable<GeoLocationSample> WatchAsync(
string sourceId,
[EnumeratorCancellation] CancellationToken ct = default)
{
// Yield a sample whenever the receiver reports a new fix.
}
}
```
Set `SupportsPush` to `false` when the source can only be polled; the binding
layer then never calls `WatchAsync` and reads through `ReadAsync` at the
Variable's sampling interval instead.
A GPOS Variable requires coordinates. If a sample has `Position` set to
`null`, the server rejects it with `BadNoDataAvailable`. If the position's
`EpsgCode` differs from the Variable's configured
`CoordinateReferenceSystem`, the binding fails rather than mis-georeference
the value silently. Set `EpsgCode` to `null` to accept the Variable's
configured coordinate reference system.
`InMemoryGeoLocationProvider` ships as a reference implementation for tests and
for servers whose positions are pushed in from elsewhere. Watching an unknown
source does not create a sample: reads return `BadNotFound` until `Update`
supplies one. `Fault` makes reads surface the supplied exception even for an
unknown source; a subsequent `Update` clears the fault and publishes its sample.
## Client
Register client factories over the managed-session registration:
```csharp
services.AddOpcUa()
.AddClient(options => { /* endpoint and application options */ })
.AddPositioningClient();
```
Direct constructors are also available:
```csharp
var rsl = new RelativeSpatialLocationClient(session, telemetry);
var gpos = new GlobalPositioningClient(session, telemetry);
await foreach (PositioningObjectEntry list in
rsl.EnumerateSpatialObjectListsAsync(ct))
{
await foreach (PositioningObjectEntry spatialObject in
rsl.EnumerateSpatialObjectsAsync(list.NodeId, ct))
{
RelativeSpatialFrameValue frame =
await rsl.ReadPositionFrameAsync(spatialObject.NodeId, ct);
}
}
await foreach (PositioningObjectEntry zone in gpos.EnumerateZonesAsync(ct))
{
GroundControlPointFitResult transform =
await gpos.ReadZoneTransformAsync(zone.NodeId, cancellationToken: ct);
}
```
`ResolveFrameToWorldAsync` follows the RSL `Base` chain, composes each
`ThreeDFrame`, and rejects missing nodes, bad status, cycles, and excessive
depth. `ObserveFrameAsync`, `ObservePositionFrameAsync`,
`ObserveNodeVersionAsync`, `ObserveGlobalPositionAsync`, and
`ObserveGlobalLocationAsync` use `IStreamingSubscription`.
GPOS structured types are registered with both the session and message-context
encodeable factories before reads, so binary `ExtensionObject` values decode to
the generated types.
Use the read-options overloads to enable strict quality checks and resource
limits. Constructors without read options use tolerant reads:
```csharp
var limits = new PositioningClientReadOptions
{
MaximumBrowseReferences = 1024,
MaximumBrowsePages = 128,
MaximumFrameChainLength = 64,
MaximumReadArrayItems = 128
};
var rsl = new RelativeSpatialLocationClient(session, telemetry, limits);
var gpos = new GlobalPositioningClient(session, telemetry, limits);
```
The options also work with `AddPositioningClient(limits)`. Strict reads reject
bad/uncertain quality, malformed values, invalid geometry and excessive arrays.
They read a frame and its Base together and bound continuation traversal.
Continuation cleanup accepts an empty successful release response while still
checking service quality; cleanup failure preserves the original read failure or cancellation.
Unencoded optional geometry fields are not treated as supplied values. Array
bounds are checked after decoding; the session's transport quotas bound
incoming messages. The supplied session remains caller-owned.
## Transform conventions
RSL uses a right-handed coordinate system:
- `A`: roll about X
- `B`: pitch about Y
- `C`: yaw about Z
- column-vector matrix: `Rz(C) * Ry(B) * Rx(A)`
`RslFrameTransform` supports conversion, composition, inversion, point
transformation, and deterministic gimbal-lock handling.
The built-in coordinate-reference-system transformer supports WGS84 /
EPSG:4326, ECEF, and local East-North-Up coordinates. Inject
`ICoordinateReferenceSystemTransformer` for another CRS.
`GroundControlPointFitter` supports rigid, similarity, and affine fits. When
elevation or rank is insufficient, it selects a horizontal 2D fit. It forces
a proper rotation for rigid and similarity modes, and rejects affine
reflections unless you enable them. The fitter reports residual, determinant,
rank, dimension, and invertibility diagnostics.
## Robot and OpenUSD sample
[`MinimalRobotServer`](../samples/Robotics/MinimalRobotServer) composes RSL and GPOS into
its Robotics node manager. Both robots publish `GlobalLocation` values and
derived local RSL frames. Each robot independently selects `Fixed`,
`FigureEight`, `Circle`, or `Shuttle` motion. RSL position/orientation drive
OpenUSD `double3` transform operators, while GPOS longitude, latitude, and
elevation drive live custom attributes.
The generic OpenUSD connector has no Positioning dependency; it handles core
`ThreeDCartesianCoordinates`, `ThreeDOrientation`, and `ThreeDFrame` values.
The server supports an explicit private PKI and finite managed run:
```powershell
$pkiRoot = Join-Path $env:TEMP 'robot-positioning-pki'
dotnet run --project samples\Robotics\MinimalRobotServer -c Release -f net10.0 -- `
--host 127.0.0.1 --port 62830 --pki-root $pkiRoot --autoaccept false --run-seconds 60
```
`--pki-root` must be absolute; `--run-seconds` accepts 1-3600 seconds. Without
the runtime option, the server runs until stopped interactively. Standalone
defaults bind all interfaces and automatically accept certificates; use
explicit secure options for controlled runs.