Files
UA-.NETStandard/docs/Positioning.md
6fe5399510 Add UaLens desktop engineering workspace (#4822)
# Description

Add **UaLens**, an Avalonia desktop engineering workspace for OPC UA
exploration, monitoring, typed editing, administration and guided
companion-specification tasks.

This is a **new draft PR for `pisrapp` against `master`**, replacing
closed, unmerged #4462 without reopening it. The shared prerequisites
(#4801, #4802, #4803, #4804 and #4806) have all merged; their
implementation is no longer part of this PR's delta.

### Scope

- Multi-document workspace with connection, security and identity
controls; browsing, subscriptions, events, alarms, history, writes and
method calls.
- Offline NodeSet2 exploration, typed scalar/array/matrix editing,
certificate/GDS and account/role tools.
- UaLens companion workflows, repository sample launching and the Aspire
AppHost.
- PubSub commissioning and capture/replay integration, plus the
UaLens-facing OpenUSD viewport and rendered Vision workflows.
- UaLens projects, test assemblies, UI assets, guides,
solution/dependency entries, desktop test wiring and delivery workflows.

Head **`6f46c51fcfef275059bbe77ef39c64c19ebc1053`** is one UaLens-only
commit directly above master
**`44a8cb277df450162c29313e2f4c862e7c683752`**. The rebase required no
conflicts. All **967 UaLens file blobs**, the full-index binary patch,
commit-message text and author/co-author attribution are unchanged from
the previously published layer. There is **no implementation delta under
`src/`**. The original history is retained in recovery refs, including
`refs/pr-stack/backups/master20261017/ualens`.

No feature was disabled, and no export/path safeguards, native
acceptance criteria, test assertions or CI gates were weakened. The four
previously recorded blank-line-at-EOF findings in the UaLens dialog
files remain unchanged; they are not reported as newly fixed.

### Validation

Fresh validation ran on the **exact PR head `6f46c51f`**, using the
existing pinned SDK **10.0.401**, normal repository analyzers and
physical/non-linked build outputs:

```powershell
$d = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe'
$b = 'D:\git\UA-.NETStandard_master\TestResults\ua-stack-feedback20261016'
$r = 'C:\Users\mschier\.copilot\session-state\abd33a2e-7139-44e1-9912-6e4451102b4e\files\ualens-new-draft20261017-net10'
$env:DOTNET_ROOT = Split-Path -Parent $d
$env:DOTNET_ROOT_X64 = $env:DOTNET_ROOT
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
$env:PATH = $env:DOTNET_ROOT + [IO.Path]::PathSeparator + $env:PATH

& $d test 'tests\Opc.Ua.Lens.Tests\Opc.Ua.Lens.Tests.csproj' --no-restore --artifacts-path $b -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter 'TestCategory!=LongRunning&TestCategory!=Stress' --logger 'trx;LogFileName=ualens-new-draft.trx' --results-directory $r --blame-hang --blame-hang-timeout 3m --blame-hang-dump-type none --verbosity minimal
# PASS, exit0: Passed: 4524, Failed: 0, Skipped: 0.
```

The saved TRX contains **4524 Passed and 44 explicit/prerequisite
NotExecuted records** (4568 total). The repository verdict helper
confirms zero failed tests, fixture setup/teardown failures or run-level
errors, without toleration. No compiler/analyzer diagnostics were
emitted. Logs, TRX and `ualens-new-draft20261017-validation.json` retain
this exact-head evidence.

**Not run for this new draft:** the full `UA.slnx` net48/net10 matrix,
fresh coverage, other desktop TFMs, application-desktop/native workflow
suites, NativeAOT, GPU/per-RID/installed-runtime or package-release
qualification. UaLens targets .NET8+; no net48 UaLens execution is
claimed. Remote CI and CodeQL are not yet claimed green. Historical
native diagnostics and successful managed tests are not substitutes for
rendered-frame or release acceptance.

The previous PR and retained session artifacts preserve the full
extraction, review, failed-attempt and diagnostic history. Historical
intermittent CI failures were not silently reclassified as root-cause
fixes; no old diagnostic grants or producer/release operations were
reused for this PR.

## Related Issues

_Reference all GitHub issues this PR addresses. If there is no issue
yet, open one and link it here._

_If this is a relatively large or complex change, a design must have
been discussed in the related tracking issue and signed off (which
becomes the Architectural Decision Record (ADR))._

- References #4461, the existing closed UaLens design/qualification
tracking issue; no new design approval is implied.
- Replaces closed, unmerged #4462 for the remaining UaLens work.
- Builds on merged prerequisites #4801, #4802, #4803, #4804 and #4806;
this PR does not close them.

## 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.
- [x] 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.

The test/coverage item remains unchecked because fresh coverage was not
collected. This draft does not claim CLA confirmation, full-matrix
clearance, native qualification or completed review.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Alexander Köpke <alexander.koepke@microsoft.com>
Co-authored-by: Copilot <Copilot@users.noreply.github.com>
2026-10-21 17:18:19 +02:00

327 lines
13 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
- [OPC UA Relative Spatial Location and Global Positioning](#opc-ua-relative-spatial-location-and-global-positioning)
- [Packages](#packages)
- [Minimal standalone server](#minimal-standalone-server)
- [Server authoring](#server-authoring)
- [Client](#client)
- [Transform conventions](#transform-conventions)
- [UaLens Positioning tool](#ualens-positioning-tool)
- [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.
## UaLens Positioning tool
[UaLens](UaLens.md#positioning) exposes these models through a dedicated
read-only tool. It discovers spatial objects, global variables on those objects
or their owners, and the GlobalLocations entry point. It does not require a
renderer or external location service.
Raw inspection preserves declared units and value evidence. World resolution
requires matching declared length units and the selected degrees/radians
interpretation across the Base chain; the tool does not convert mixed units.
It bounds chains to 64 frames and checks exposed spatial-list NodeVersions.
These checks do not provide atomic telemetry snapshots.
Zone previews use a copied, bounded ground-control-point calibration and
single-use preparation. A 2D fit requires local Z = 0; only a 3D fit produces
elevation. Connection changes, changed calibration, cancellation or the fixed
five-minute preparation deadline prevent publication. The preview never writes
a device position.
## 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.
UaLens supplies loopback binding, private PKI, disabled automatic trust
and a finite lifetime. Its Positioning workflow does not authorize robot
operation commands.