mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
# 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>
327 lines
13 KiB
Markdown
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.
|