mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 14:43:08 +08:00
7e25e1a3d7
# 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.
302 lines
12 KiB
Markdown
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.
|