mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 09:06:37 +08:00
b3c1e60f46
# Description Extract the AMB layer from #4462 onto the client/server predecessor #4802, then address the four review findings in `6428686ae45724510bd6bd00619f4992f3d36e46`. The initial extraction (`95b0caa1ac30d1fdacaf7a780f19c5f97e6fd02d`) contained exactly 19 paths: 12 AMB client files, four AMB test files, two sample entry points and `docs/AMB.md`. Its code/tests matched the frozen source `5b03e9afa492cdb36049ee52ae844f630f89d8db...390c4a7a51b71da40caec2d22ef74518bbe670ce`; only the two agreed UaLens-only documentation passages were omitted. The review follow-up intentionally changes that extracted implementation and adds the related factory/registration tests and package documentation. No UaLens, Machinery, Pumps, Positioning, native-rendering, build or CI changes are introduced by this layer. The AMB client provides opt-in bounded traversal, strict response/type validation and original `AssetReadValue` evidence while preserving tolerant defaults. The samples expose validated host, port, PKI root, certificate auto-accept and bounded-runtime options, with unchanged standalone defaults. The review corrections are: - Accept header-validated empty BrowseNext release responses required by Part 4 §5.9.3.2, as well as the stack's single-result response. Invalid cleanup results still preserve the original failure. - Keep event observation tolerant of denied/limited maintenance-class discovery without caching incomplete classes. Subsequent strict reads retry discovery and still report errors. - Accept typed-null ExtensionObjects only in the nullable structure-field checks, preserving original evidence without weakening scalar validation. - Pass optional immutable limits through `AddAssetManagementClient`, the registered factory/delegate and directly constructed factories. Preserve existing overloads and tolerant behavior when options are omitted; reject invalid limits before opening a session. ### Review regression evidence | Requirement | Evidence | | --- | --- | | Accept empty cleanup responses without masking cancellation or limits | `OneBeyondReferenceLimitReleasesAcquiredContinuationAsync` and `CancellationWhileAwaitingNextReleasesTheSubmittedContinuationAsync`, each with empty and single-result responses; malformed responses remain covered | | Preserve observation fallback and do not cache incomplete classes | `ObservationFallsBackWithoutCachingIncompleteClassesAsync`: both observation APIs, denied browsing and exhausted budget, correct filter/event evidence, and a subsequent strict rediscovery | | Preserve typed-null structure evidence without weakening scalar checks | `ContextAndEntryReadsKeepSourceAndNullStructureEvidenceAsync`, `MaintenanceTypedNullStructuresKeepOriginalEvidenceAsync`, `ScalarHealthRejectsTypedNullStructuresAsync`, plus wrong-non-null-structure rejection tests | | Apply configured hosted limits while retaining tolerant defaults | `HostedClientOptionsReachClientsAsync`: registered factory, registered delegate and direct factory, singleton and builder registration, actual one-reference limit and read evidence; `InvalidHostedLimitsAreRejectedBeforeOpeningASession` | ### Validation All commands below ran in this layer's worktree with the existing pinned .NET SDK 10.0.401. No SDK installation, global.json change, registry/path-policy change, test suppression or analyzer suppression was made. The short owned NUnit directory `D:\a61r` and its generated certificates/state were removed afterward. **Latest full AMB suite: Passed: 285, Failed: 0, Skipped: 0 on each framework.** Both builds emitted no compiler/analyzer diagnostics. TRX files confirm the regression cases executed on both TFMs. ```powershell & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' test 'tests\Opc.Ua.AMB.Tests\Opc.Ua.AMB.Tests.csproj' --no-restore --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 --nologo --verbosity minimal --logger 'trx;LogFileName=amb-feedback-net10.trx' --results-directory 'C:\Users\mschier\.copilot\session-state\61d7ed73-c89e-406a-a049-1d8ba9681308\files\TestResults\feedback-net10' -- 'NUnit.WorkDirectory=D:\a61r' & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' test 'tests\Opc.Ua.AMB.Tests\Opc.Ua.AMB.Tests.csproj' --configuration Release --framework net48 -p:CustomTestTarget=net48 --nologo --verbosity minimal --logger 'trx;LogFileName=amb-feedback-net48.trx' --results-directory 'C:\Users\mschier\.copilot\session-state\61d7ed73-c89e-406a-a049-1d8ba9681308\files\TestResults\feedback-net48' -- 'NUnit.WorkDirectory=D:\a61r' ``` **Focused regression run: Passed: 127, Failed: 0, Skipped: 0.** Before the production fixes, the focused repro executed 112 cases with 16 failures reproducing all four reported defects. Additional registration/validation cases were included in the final run. ```powershell & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' test 'tests\Opc.Ua.AMB.Tests\Opc.Ua.AMB.Tests.csproj' --no-restore --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 --nologo --verbosity minimal --filter 'FullyQualifiedName~AmbBoundedClientTests|FullyQualifiedName~AmbClientTests|FullyQualifiedName~HostedClientOptionsReachClientsAsync' --logger 'trx;LogFileName=amb-feedback-focused.trx' --results-directory 'C:\Users\mschier\.copilot\session-state\61d7ed73-c89e-406a-a049-1d8ba9681308\files\TestResults\feedback-focused' -- 'NUnit.WorkDirectory=D:\a61r' ``` **Whitespace and newly introduced style findings: passed.** `git diff --check` and the following commands exited 0: ```powershell & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' format whitespace --folder --verify-no-changes --verbosity quiet --include 'src\Opc.Ua.AMB.Client\AmbClient.Bounds.cs' 'src\Opc.Ua.AMB.Client\AmbClient.Health.cs' 'src\Opc.Ua.AMB.Client\AmbClient.Structure.cs' 'src\Opc.Ua.AMB.Client\AmbClient.Values.cs' 'src\Opc.Ua.AMB.Client\AmbClientFactory.cs' 'src\Opc.Ua.AMB.Client\Hosting\OpcUaAmbClientBuilderExtensions.cs' 'tests\Opc.Ua.AMB.Tests\AmbBoundedClientTests.cs' 'tests\Opc.Ua.AMB.Tests\AmbBoundedClientTests.Traversal.cs' 'tests\Opc.Ua.AMB.Tests\AmbBoundedClientTests.Values.cs' 'tests\Opc.Ua.AMB.Tests\AmbClientServerE2eTests.cs' 'tests\Opc.Ua.AMB.Tests\AmbClientTests.cs' $env:CustomTestTarget = 'net10.0' $env:Configuration = 'Release' & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' format style 'tests\Opc.Ua.AMB.Tests\Opc.Ua.AMB.Tests.csproj' --no-restore --verify-no-changes --severity info --diagnostics IDE0039 --verbosity quiet --include 'tests\Opc.Ua.AMB.Tests\AmbClientServerE2eTests.cs' 'tests\Opc.Ua.AMB.Tests\AmbClientTests.cs' ``` The broader formatter check was **not clean**. It reported pre-existing informational suggestions and design-time `NUnit1003` diagnostics for generated browse-name constants, including pre-existing test cases. Those diagnostics do not occur in the actual builds, which compile and execute those cases successfully. Two new local-function suggestions were corrected and rechecked above; no unrelated baseline cleanup or suppression was included. ```powershell $env:CustomTestTarget = 'net10.0' & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' format 'tests\Opc.Ua.AMB.Tests\Opc.Ua.AMB.Tests.csproj' --no-restore --verify-no-changes --severity info --verbosity quiet --include 'src\Opc.Ua.AMB.Client\AmbClient.Bounds.cs' 'src\Opc.Ua.AMB.Client\AmbClient.Health.cs' 'src\Opc.Ua.AMB.Client\AmbClient.Structure.cs' 'src\Opc.Ua.AMB.Client\AmbClient.Values.cs' 'src\Opc.Ua.AMB.Client\AmbClientFactory.cs' 'src\Opc.Ua.AMB.Client\Hosting\OpcUaAmbClientBuilderExtensions.cs' 'tests\Opc.Ua.AMB.Tests\AmbBoundedClientTests.cs' 'tests\Opc.Ua.AMB.Tests\AmbBoundedClientTests.Traversal.cs' 'tests\Opc.Ua.AMB.Tests\AmbBoundedClientTests.Values.cs' 'tests\Opc.Ua.AMB.Tests\AmbClientServerE2eTests.cs' 'tests\Opc.Ua.AMB.Tests\AmbClientTests.cs' ``` <details> <summary>Initial extraction validation and environment history</summary> The original extraction passed all 263 AMB tests on both TFMs. Its initial net48 run had one unchanged maintenance-test server-start timeout; shortening only NUnit.WorkDirectory resolved it, consistent with certificate-store path lengths. This follow-up used a short directory from the start and all 285 cases passed on both frameworks. A no-restore test invocation during the follow-up initially executed no tests; it was not counted as passing evidence. A no-restore build identified `NETSDK1005` (missing net10.0 target assets after switching TFMs). The normal test invocation restored the target assets. Initial regression-test compilation issues were corrected before capturing the 16 failing behavioral cases. Both samples were built during extraction on both TFMs; each command below passed with 0 warnings and 0 errors. They were not rebuilt during this review follow-up, which does not modify the samples. ```powershell & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' build 'samples\AMB\AmbClient\AmbClient.csproj' --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 --nologo --verbosity quiet & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' build 'samples\AMB\AmbServer\AmbServer.csproj' --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 --nologo --verbosity quiet & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' build 'samples\AMB\AmbClient\AmbClient.csproj' --configuration Release --framework net48 -p:CustomTestTarget=net48 --nologo --verbosity quiet & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' build 'samples\AMB\AmbServer\AmbServer.csproj' --configuration Release --framework net48 -p:CustomTestTarget=net48 --nologo --verbosity quiet ``` </details> This remains a draft: the complete `UA.slnx` net48/net10.0 matrix, coverage, CI/CodeQL and CLA status are not established by this focused validation. No coverage increase or complete-solution pass is claimed. The feedback commit is based on the original AMB extraction; integration of later lower-layer updates is coordinated separately, without rewriting this branch or native stack 4807. ## Related Issues - References #4461, the existing tracking issue; this PR does not close it. - Extracted from #4462 at frozen head `390c4a7a51b71da40caec2d22ef74518bbe670ce`; the source PR is not closed by this PR. - Immediate predecessor: #4802 (`marcschier-extract-client-and-server`), above core/types #4801. The original extraction parent was `86ff10d3994f132f7343a929abdae209bc2e69b7`. This is an isolated extraction and review correction of the existing tracked implementation, not a claim of new architectural sign-off. ## 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. - [x] I have addressed **all** PR feedback received.
682 lines
39 KiB
Markdown
682 lines
39 KiB
Markdown
# OPC UA Asset Management Basics (OPC 10000-110)
|
|
|
|
The `Opc.Ua.AMB*` packages implement
|
|
[OPC 10000-110](https://reference.opcfoundation.org/specs/OPC-10000-110),
|
|
Asset Management Basics, version 1.01.1. AMB is not a model of a kind of
|
|
device: it is a set of rules and building blocks that make any object of a
|
|
server a *manageable asset* - a Device Integration device, a Machinery
|
|
machine, a pump or a scale - so that an asset management system can find it,
|
|
identify it, follow its health and maintenance, link it to its documentation
|
|
and place it in the plant.
|
|
|
|
A server registers its assets with a sidecar node manager; a client discovers
|
|
them and reads them back into records.
|
|
|
|
## Contents
|
|
|
|
- [Library layout](#library-layout)
|
|
- [The model](#the-model)
|
|
- [Where an asset lives](#where-an-asset-lives)
|
|
- [Quick start - server](#quick-start---server)
|
|
- [Quick start - client](#quick-start---client)
|
|
- [Server hosting](#server-hosting)
|
|
- [Builder reference](#builder-reference)
|
|
- [Discovery](#discovery)
|
|
- [Health](#health)
|
|
- [Maintenance](#maintenance)
|
|
- [Documentation links](#documentation-links)
|
|
- [Locations, version information, classification and structure](#locations-version-information-classification-and-structure)
|
|
- [Conformance matrix](#conformance-matrix)
|
|
- [Client reference](#client-reference)
|
|
- [Samples](#samples)
|
|
- [Testing](#testing)
|
|
- [Troubleshooting](#troubleshooting)
|
|
- [Known limitations](#known-limitations)
|
|
- [Model provenance](#model-provenance)
|
|
- [Generator gaps this model uncovered](#generator-gaps-this-model-uncovered)
|
|
- [See also](#see-also)
|
|
|
|
## Library layout
|
|
|
|
| Package | Contents |
|
|
| --- | --- |
|
|
| `Opc.Ua.AMB` | The source-generated AMB model (interfaces, condition classes, the maintenance state machine, `DocumentationLinksType`, the reference types, `RootCauseDataType`, `NameNodeIdDataType`, `AddOpcUaAMB`), `AmbBrowseNames`, `AmbConditionClass` and the shared contracts `MaintenanceStateKind`, `AssetFaultSeverity` and `AssetLocationKind` |
|
|
| `Opc.Ua.AMB.Server` | `AmbNodeManager`, the asset registry `IAssetManagement` with its builders and handles, the configuration stores, and `AddAssetManagement()` / `ConfigureAssetManagement()` |
|
|
| `Opc.Ua.AMB.Client` | `AmbClient` for discovery, identification, health alarms, maintenance, documentation links, locations and structure, plus `AddAssetManagementClient()` |
|
|
|
|
The model package depends on the base namespace only. The server and client
|
|
packages use the OPC 10000-100 Device Integration building blocks AMB applies
|
|
to an asset - identification, `DeviceHealth`, the health alarm types - so a
|
|
server needs the DI namespace; registering an asset without it fails.
|
|
|
|
## The model
|
|
|
|
| Model URI | C# namespace | `Namespaces` member | Model loader |
|
|
| --- | --- | --- | --- |
|
|
| `http://opcfoundation.org/UA/AMB/` | `Opc.Ua.AMB` | `AMB` | `AddOpcUaAMB` |
|
|
|
|
The NodeSet has 92 nodes. AMB defines no event types and attaches nothing to
|
|
the types of other models; it defines three building blocks an application
|
|
applies:
|
|
|
|
| Building block | Applied to | Section |
|
|
| --- | --- | --- |
|
|
| `IRootCauseIndicationType` with `PotentialRootCauses` | alarm types | 9.4 |
|
|
| `IMaintenanceEventType` with `MaintenanceState` and eight optional details | condition types | 12.2 |
|
|
| `DocumentationLinksType` | assets, as an AddIn (`0:HasAddIn`) | 10.5 |
|
|
|
|
Its instances are the alias category `Assets` below `0:Aliases` with
|
|
`AssetsByProductInstanceUri` and `AssetsByAssetId`, and the location entry
|
|
points `HierarchicalLocations` and `OperationalLocations` below `0:Locations`.
|
|
The fourteen condition classes (`ConnectionFailureConditionClassType` and so
|
|
on) are listed in `AmbConditionClass`.
|
|
|
|
## Where an asset lives
|
|
|
|
AMB concerns assets across every model of a server, while the Device
|
|
Integration address space has exactly one owner (`AddOpcUaDi`, `AddMachinery`,
|
|
`AddPumps` or `AddScales`). The AMB node manager is therefore a sidecar: it
|
|
loads the AMB model and serves the alias categories, the location hierarchies
|
|
and the server-specific types, but the assets stay in the node manager that
|
|
owns them.
|
|
|
|
Everything AMB adds to an asset - `DeviceHealth`, alarms, maintenance
|
|
conditions, the DocumentationLinks AddIn, properties - is created in that
|
|
owner. The owner registers each asset with the shared `IAssetManagement`
|
|
registry from its setup; the registry is the meeting point of the managers.
|
|
|
|
```
|
|
DI / Machinery / Pumps / Scales node manager AMB node manager (sidecar)
|
|
DeviceSet/Pump ---- registered with ----> Aliases/Assets/AssetsBy...
|
|
2:DeviceHealth, 2:DeviceHealthAlarms Locations/HierarchicalLocations
|
|
AMB:DocumentationLinks, AssetId, ... server-specific alarm types
|
|
```
|
|
|
|
## Quick start - server
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddServer(options => { /* application name, endpoints, PKI */ })
|
|
.AddOpcUaDi() // or AddMachinery, AddPumps, AddScales
|
|
.AddAssetManagement(options => options.UseFileSystemStores("amb-state"))
|
|
.ConfigureDevicesFor<DiNodeManager>(async context =>
|
|
{
|
|
IDeviceBuilder<DeviceState> device = await context.CreateDeviceAsync(
|
|
new QualifiedName("Sensor_1", context.Manager.InstanceNamespaceIndex));
|
|
device.WithIdentification(i => i.ProductInstanceUri = "urn:acme:sensor:4711");
|
|
|
|
await device.RegisterAsAssetAsync(
|
|
context.GetRequiredService<IAssetManagement>(),
|
|
asset => asset
|
|
.WithConfigurableAssetId()
|
|
.WithDeviceHealth(deriveFromAlarms: true)
|
|
.WithHealthAlarm("Fieldbus", AssetHealthAlarmKind.Failure, AmbConditionClass.ConnectionFailure)
|
|
.WithMaintenance("AnnualInspection", AmbConditionClass.Inspection,
|
|
details => details.PlannedDate = new DateTime(2027, 3, 1, 8, 0, 0, DateTimeKind.Utc))
|
|
.WithDocumentationLinks(links => links
|
|
.Add("Manual", "https://acme.example/sensor/manual.pdf")
|
|
.AllowUserLinks())
|
|
.LocatedIn(AssetLocationKind.Hierarchical, "Plant1/Hall3/Line2"),
|
|
context.CancellationToken);
|
|
});
|
|
```
|
|
|
|
Registering checks that the asset publishes a non-empty `ProductInstanceUri`,
|
|
on the asset or in its `2:Identification` group (§7). The returned
|
|
`IAssetHandle` drives the asset at runtime:
|
|
|
|
```csharp
|
|
IAssetHealthAlarm fieldbus = handle.Health!.Alarms[0];
|
|
await fieldbus.RaiseAsync(700, new LocalizedText("The fieldbus is down."),
|
|
[AssetRootCauses.Of(cableId, new LocalizedText("Cable cut"))]);
|
|
await handle.Maintenance!.Activities[0].StartAsync();
|
|
```
|
|
|
|
## Quick start - client
|
|
|
|
```csharp
|
|
AmbClient amb = session.AssetManagement(telemetry);
|
|
|
|
foreach (NodeId asset in await amb.DiscoverAssetsAsync())
|
|
{
|
|
AssetSnapshot snapshot = await amb.ReadAssetAsync(asset);
|
|
Console.WriteLine($"{snapshot.Identification.ProductInstanceUri}: {snapshot.DeviceHealth}");
|
|
}
|
|
|
|
await foreach (AssetAlarmRecord alarm in amb.ObserveHealthAlarmsAsync())
|
|
{
|
|
Console.WriteLine($"{alarm.SourceName}/{alarm.ConditionName} {alarm.Severity} {alarm.Message.Text}");
|
|
}
|
|
```
|
|
|
|
## Server hosting
|
|
|
|
### `AddAssetManagement` and `ConfigureAssetManagement`
|
|
|
|
`AddAssetManagement(options => ...)` registers the AMB node manager factory,
|
|
the options, the shared `AssetManagement` registry (also as
|
|
`IAssetManagement`) and the configuration store. It claims no DI ownership, so
|
|
it is combined with the registration that does, in either order.
|
|
|
|
`ConfigureAssetManagement(context => ...)` runs inside the AMB node manager
|
|
while it builds its address space. The context gives the manager, the
|
|
registry, the fluent builder of the AMB manager and the services; use it to
|
|
define location hierarchies or to register objects the AMB manager itself
|
|
creates. A failing configurator aborts the start; the server reports
|
|
`BadInternalError` and logs the cause.
|
|
|
|
Without hosting, create an `AssetManagement` and pass it to
|
|
`new AmbNodeManager(server, configuration, assetManagement)`.
|
|
|
|
### Options
|
|
|
|
| Option | Default | Meaning |
|
|
| --- | --- | --- |
|
|
| `InstanceNamespaceUri` | `urn:opcua-netstandard:amb:instances` | Namespace of the alias objects and location objects |
|
|
| `TypeNamespaceUri` | `urn:opcua-netstandard:amb:types` | Namespace of the server-specific alarm and condition types |
|
|
| `RequireProductInstanceUri` | `true` | Refuse assets without `ProductInstanceUri` |
|
|
| `Discovery` | `AssetDiscovery.All` | The alias categories assets are listed in |
|
|
| `MaxAssetIdLength` | 255 (at least 40) | Longest AssetId a client may write, in Unicode characters |
|
|
| `UseServerDefinedAlarmTypes` | `true` | Apply the AMB interfaces on server-specific types; without them the Root Causes and maintenance units are not met (see [Health](#health)) |
|
|
| `MaxDocumentationLinkLength` | 2048 (at least 255) | Longest link a client may write or add |
|
|
| `MaxUserLinksPerAsset` | 16 (at least 2) | How many links users may add to one asset |
|
|
| `MaxDocumentationLinkNameLength` | 128 | Longest browse name and display name of a link a user adds |
|
|
| `MaxDocumentationLinkDescriptionLength` | 1024 | Longest description of a link a user adds |
|
|
| `MaxLocationLength` | 1024 | Longest value a client may write to a writable location Property |
|
|
| `AuthorizeLinkEdit` | authenticated users | Who may change documentation links |
|
|
| `UseFileSystemStores(directory)` | memory | Persist what clients configure below a directory |
|
|
|
|
### Persistence
|
|
|
|
What clients configure - the AssetId, editable and added documentation links,
|
|
writable locations and the local time - is kept in an
|
|
`IAssetConfigurationStore` under the asset's `ProductInstanceUri`.
|
|
`MemoryAssetConfigurationStore` keeps it for the life of the process;
|
|
`FileSystemAssetConfigurationStore` (`UseFileSystemStores`) writes one JSON
|
|
file per asset and restores the values on start. A document is written to a
|
|
temporary file first and then replaces the old one, so a crash leaves either
|
|
the old or the complete new document. A restart has to keep the
|
|
application identity, because the namespaces of persisted NodeIds are stored
|
|
by URI.
|
|
|
|
### When to register
|
|
|
|
Health alarms and maintenance conditions are created while the node manager
|
|
that owns the asset builds its address space - from `ConfigureDevicesFor`, an
|
|
`IMachineryConfigurator`, `ConfigurePumps`, `ConfigureScales` or
|
|
`ConfigureAssetManagement`. Afterwards the fluent builder is sealed and
|
|
creating them fails. Identification, discovery, links and locations work for
|
|
assets registered later as well.
|
|
|
|
## Builder reference
|
|
|
|
`IAssetBuilder`, passed to `RegisterAssetAsync` (and to
|
|
`IDeviceBuilder<T>.RegisterAsAssetAsync`):
|
|
|
|
| Method | Adds | Section |
|
|
| --- | --- | --- |
|
|
| `WithConfigurableAssetId(default)` | a writable, persistent `2:AssetId` | 7 |
|
|
| `WithDeviceHealth(initial, deriveFromAlarms)` | `2:DeviceHealth`, with `2:IDeviceHealthType` where the type lacks it | 9.2 |
|
|
| `WithHealthAlarm(name, kind, class)` | an OPC 10000-100 health alarm with root causes, and `2:DeviceHealth` following the alarms unless `WithDeviceHealth` sets it up | 9.2 to 9.4 |
|
|
| `WithMaintenance(name, class, details, onStateChanged)` | a maintenance condition with its state machine | 12 |
|
|
| `WithDocumentationLinks(links)` | the DocumentationLinks AddIn | 10.5 |
|
|
| `WithVersionInformation(hardware, software, counter)` | revisions and `RevisionCounter` | 10.2 |
|
|
| `WithLocation(kind, value, writable)` | `HierarchicalLocation`, `OperationalLocation` or `DigitalLocation` | 13.3.2, 13.4.2, 13.5 |
|
|
| `WithLocalTime(offset, daylightSaving, writable)` | `0:LocalTime` | 13.2 |
|
|
| `LocatedIn(kind, "A/B/C")` | membership in a location hierarchy | 13.3.3, 13.4.3 |
|
|
| `ClassifiedAs(dictionaryEntry)` | a `0:HasDictionaryEntry` reference to a dictionary entry (`DefineDictionaryEntryAsync`) | 11 |
|
|
| `WithRequirements(entries)`, `WithCapabilities(entries)` | the two folders | 10.6, 10.7 |
|
|
| `RelatesTo(referenceType, target)` | a sub-asset (hierarchical) or a relation (non-hierarchical) | 14 |
|
|
|
|
Properties the asset publishes already are used rather than duplicated, and an
|
|
existing write handler of the `AssetId` keeps running.
|
|
|
|
## Discovery
|
|
|
|
Every registered asset with a `ProductInstanceUri` is listed in
|
|
`AssetsByProductInstanceUri`, and every asset that has a `2:AssetId` in
|
|
`AssetsByAssetId` (§8.2): under its value, or as `NoAssetIdAssigned` while the
|
|
value is empty (§8.2.3). An asset without an AssetId is not listed there. One
|
|
`AliasNameType` object stands for each alias name and references its assets
|
|
with `0:AliasFor` - assets that share a name share the object (§8.1); each
|
|
asset gets the inverse `0:HasAlias`. `FindAlias` works on each of the three
|
|
categories; a search on `Assets` covers both subcategories.
|
|
|
|
Listing, moving or withdrawing an asset changes the `NodeVersion` of the
|
|
category and is announced with a `GeneralModelChangeEvent`. Writing a
|
|
configurable AssetId moves the asset once the value is persisted; the writes of
|
|
one asset are serialized, so the stored value, the value clients read and the
|
|
alias never disagree. A
|
|
`ProductInstanceUri` or an AssetId outside the binding that the application
|
|
changes later moves the asset as well, once the change is reported through
|
|
`ClearChangeMasks`; an asset registered without a `ProductInstanceUri`
|
|
(`RequireProductInstanceUri = false`) is listed then. An AssetId that clients
|
|
cannot write is logged as a warning: §7 asks for a writable one.
|
|
|
|
## Health
|
|
|
|
`DeviceHealth` is set by the application (`SetDeviceHealthAsync`) or, with
|
|
`deriveFromAlarms: true`, follows the active alarms: failure before function
|
|
check before out of specification before maintenance required. An asset with
|
|
health alarms always has `DeviceHealth` (§9.2); without `WithDeviceHealth` it
|
|
follows the alarms. Its source
|
|
timestamp changes only with the value, because it marks when the asset
|
|
entered the state (§9.2).
|
|
|
|
A health alarm is an instance of an OPC 10000-100 alarm type
|
|
(`AssetHealthAlarmKind`: `Failure`, `CheckFunction`, `OffSpec`,
|
|
`MaintenanceRequired`), created on the asset so that `SourceNode` and
|
|
`SourceName` identify the asset (§9.3), listed in the `2:DeviceHealthAlarms`
|
|
folder, and reporting an AMB condition class.
|
|
|
|
OPC 10000-110 applies `IRootCauseIndicationType` to alarm *types*. By default
|
|
the manager therefore creates `AssetFailureAlarmType`,
|
|
`AssetCheckFunctionAlarmType`, `AssetOffSpecAlarmType` and
|
|
`AssetMaintenanceRequiredAlarmType` in its type namespace, each a subtype of
|
|
the DI alarm type that implements the interface; a client filtering for the DI
|
|
type still receives the alarms. `UseServerDefinedAlarmTypes = false` keeps the
|
|
DI types and references the interface from each alarm instead; the alarm types
|
|
then do not implement the interface, so the server does not claim "AMB Asset
|
|
Health Status Root Causes" (§9.4.1).
|
|
|
|
`RaiseAsync(severity, message, rootCauses)` takes a severity of one of the
|
|
active bands of OPC 10000-110 Table 15 (`AssetFaultSeverities`):
|
|
|
|
| Severity | Category |
|
|
| --- | --- |
|
|
| 801 to 1000 | Critical fault |
|
|
| 601 to 800 | Major recoverable fault |
|
|
| 401 to 600 | Minor recoverable fault |
|
|
| 301 to 400 | Maintenance needed |
|
|
| 201 to 300 | Limited resource capacity near limit |
|
|
| 1 to 200 | Inactive |
|
|
|
|
The potential root causes follow §9.4.2: no list means the cause is unknown
|
|
and becomes one "unknown" entry, `AssetRootCauses.Self` (an empty list) means
|
|
the alarm itself is the cause. `ClearAsync` drops the severity into the
|
|
inactive band; an unacknowledged alarm stays retained until a client
|
|
acknowledges it, and the acknowledgement is reported as an event.
|
|
|
|
## Maintenance
|
|
|
|
A maintenance activity is a condition of the server-specific
|
|
`AssetMaintenanceActivityConditionType`, a subtype of
|
|
`2:MaintenanceRequiredAlarmType` that implements `IMaintenanceEventType`
|
|
(`UseServerDefinedAlarmTypes = false`: the DI type with the interface on the
|
|
instance, which does not meet "AMB Current and Future Maintenance Activities",
|
|
since §12.1 asks the condition type to implement it). It reports a
|
|
maintenance condition class and moves through the
|
|
`MaintenanceEventStateMachineType`:
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> Planned
|
|
Planned --> Executing: StartAsync
|
|
Executing --> Finished: FinishAsync
|
|
Finished --> Planned: ReplanAsync
|
|
```
|
|
|
|
| State | Condition |
|
|
| --- | --- |
|
|
| Planned | active, retained, severity 301 |
|
|
| Executing | active, retained, severity 301 |
|
|
| Finished | inactive, severity 100, retained until acknowledged |
|
|
|
|
The description of `MaintenanceActivityDetails` is the message of the events
|
|
(§12.1); a failed execution is told through the message of `FinishAsync`.
|
|
The details (planned date, estimated downtime, supplier, qualification,
|
|
replaced and serviced parts, method, configuration change) are published once
|
|
they are set, and `UpdateAsync` changes them in any state. Every transition and
|
|
update reports an event, so the event history keeps planned and actual values
|
|
apart. A transition the state machine does not allow - starting a finished
|
|
activity, for example - fails with `BadInvalidState`, and so does one a handler
|
|
on `OnBeforeTransition` of the `MaintenanceState` refuses. Those handlers, the
|
|
one on `OnAfterTransition` and the delegate of `UpdateAsync` run outside the
|
|
lock of the activity, so they may read it; a transition whose activity moved
|
|
while its guard ran is refused. An
|
|
`onStateChangedAsync` callback ties the activity to the application, for
|
|
example to switch a machine into its `Maintenance` operation mode, as the
|
|
sample does.
|
|
|
|
## Documentation links
|
|
|
|
`WithDocumentationLinks` adds the AddIn `AMB:DocumentationLinks`:
|
|
|
|
- `Add(name, uri)` - a link of the manufacturer; read only.
|
|
- `AddEditable(name, default)` - a link users change to one of their own
|
|
("AMB DocumentationLinks Edit Base").
|
|
- `AllowUserLinks()` - the `AddLink` and `RemoveLink` methods ("AMB
|
|
DocumentationLinks Edit Advanced").
|
|
|
|
`AddLink` persists the link before it creates the variable, and the variable
|
|
keeps its NodeId across restarts; since §10.5.3 asks for that,
|
|
`AllowUserLinks()` needs a persistent store - with the memory store,
|
|
registering the asset fails with `BadConfigurationError`. `RemoveLink` removes
|
|
only links `AddLink` created. An editable link nobody has set is published as
|
|
null and, like a cleared one, does not count for "AMB DocumentationLinks
|
|
Base". Both answer with `BadUserAccessDenied` and `BadInvalidArgument` as
|
|
§10.5 defines; `AddLink` refuses names, display names and descriptions longer
|
|
than the options allow. Links restored after a restart are cut to the limits
|
|
then in force, with a warning in the log, and the stores refuse any value
|
|
longer than `AssetConfigurationNames.MaxValueLength`. `AuthorizeLinkEdit` decides who may write, add and remove
|
|
links; by default every authenticated user may and an anonymous one may not.
|
|
The `UserAccessLevel` of an editable link and the `UserExecutable` of `AddLink`
|
|
and `RemoveLink` show a user who may not change links that he cannot.
|
|
|
|
OPC 10000-110 gives a client no way to tell the links of the manufacturer from
|
|
the ones users added, although `RemoveLink` accepts only the latter. §10.5.1
|
|
lets vendors add Properties with metadata to the links, so every link `AddLink`
|
|
created - and only such a link - carries the `Boolean` Property
|
|
`UserLink` (`DocumentationLinkProperties.UserLink`) with the value `true`. Its
|
|
browse name is qualified with the namespace of the server-specific types
|
|
(`AmbServerOptions.TypeNamespaceUri`), not with the AMB namespace, and its
|
|
NodeId follows from the link's, so it is the same after a restart.
|
|
`DocumentationLinkRecord.IsUserLink` reports it on the client, which compares
|
|
the name only, as the namespace is configurable.
|
|
|
|
## Locations, version information, classification and structure
|
|
|
|
`LocatedIn(kind, "Plant1/Hall3/Line2")` creates the levels below
|
|
`HierarchicalLocations` or `OperationalLocations` in the AMB manager - the
|
|
first organized by the entry point, deeper ones as components, as §13.3.3
|
|
recommends - and connects the deepest level and the asset with
|
|
`HierarchicalContains` or `OperationalContains` in both directions (§13.1).
|
|
`IAssetManagement.DefineLocationAsync` creates a location without an asset.
|
|
Unregistering an asset removes the `*Contains` references; the locations
|
|
stay.
|
|
|
|
The version information writes the hardware and software revisions and the
|
|
`RevisionCounter` onto the properties the asset has and creates the missing
|
|
ones; `IAssetHandle.IncrementRevisionCounterAsync` counts configuration
|
|
changes. A Device Integration device has all three properties, but their
|
|
OPC 10000-100 defaults - empty revisions and a `RevisionCounter` of -1 - say
|
|
that the device does not provide them and do not count as version
|
|
information. Asking for version information starts a counter of -1 at 0.
|
|
|
|
Requirements and capabilities are folders in the AMB namespace whose entries
|
|
may reference entries of external dictionaries such as IEC CDD or ECLASS;
|
|
`ClassifiedAs` classifies the asset itself. OPC 10000-19 asks the target of
|
|
`HasDictionaryEntry` to be a `DictionaryEntryType` object:
|
|
`IAssetManagement.DefineDictionaryEntryAsync(irdi)` creates an
|
|
`IrdiDictionaryEntryType` object below `Server/Dictionaries` whose NodeId is the
|
|
IRDI in the namespace `http://opcfoundation.org/UA/Dictionary/IRDI`
|
|
(`AmbServerOptions.IrdiNamespaceUri`), so an asset is classified with
|
|
`new ExpandedNodeId(irdi, AmbServerOptions.IrdiNamespaceUri)`, before or after
|
|
the entry is defined. "AMB Classification" counts only such references. `RelatesTo` with a hierarchical
|
|
reference makes a sub-asset, with a non-hierarchical one - `0:Utilizes`,
|
|
`0:IsPhysicallyConnectedTo` and the other OPC 10000-23 references - a
|
|
relation; the target gets the inverse reference.
|
|
|
|
## Conformance matrix
|
|
|
|
### Server
|
|
|
|
The manager advertises the conformance units the registered assets meet, and
|
|
the AMB Base Asset Management Server Facet
|
|
(`http://opcfoundation.org/UA-Profile/AMB/Server/BaseServer`) exactly when
|
|
its only mandatory unit, AMB Asset Identification, is met; the other 27 units
|
|
are optional (§15.2). A unit whose rule reads "every asset" is met only with
|
|
at least one asset registered. The units are qualified names in namespace 0,
|
|
as Device Integration and Machinery publish theirs. The capabilities describe
|
|
the current configuration (OPC 10000-5 §6.3.2): registering or unregistering an
|
|
asset publishes the list again, with the units and the facet the assets meet
|
|
now, so a unit they no longer meet is withdrawn.
|
|
|
|
In the Tests column, "session" means a test checks the unit through a real
|
|
client session, "in-process" that a test checks it inside the server only.
|
|
|
|
| Conformance unit (Table 54) | Status | Rule the server applies | Tests |
|
|
| --- | --- | --- | --- |
|
|
| AMB Asset Identification (mandatory) | Implemented | every asset has a ProductInstanceUri, on the asset or in `2:Identification` | session |
|
|
| AMB Configurable Asset Identification | Implemented | every asset has a writable AssetId of at least 40 characters, and the store is persistent | session |
|
|
| AMB Asset Discovery by ProductInstanceUri | Implemented | every asset is listed in the category | session |
|
|
| AMB Asset Discovery by AssetId | Implemented | every asset with an AssetId is listed in the category, and at least one has one | session |
|
|
| AMB Asset Health Status Base | Implemented | every asset has `2:DeviceHealth` | session |
|
|
| AMB Asset Health Status Alarms | Implemented | every asset has a health alarm | session |
|
|
| AMB Asset Health Status Root Causes | Implemented | as Alarms; the alarm types implement `IRootCauseIndicationType` (`UseServerDefinedAlarmTypes`) | session |
|
|
| AMB Asset Health Status Alarm Categories | Implemented | as Alarms; every alarm reports a condition class | session |
|
|
| AMB Asset Health Tracking Overall Asset Status | Not implemented yet (history) | - | - |
|
|
| AMB Asset Health Tracking Events | Not implemented yet (history) | - | - |
|
|
| AMB Version Information | Implemented | every asset has a non-empty hardware or software revision and a RevisionCounter of at least 0 | session |
|
|
| AMB Operation Counters | Inspected only; AMB defines no counters (§10.3) | an asset has `2:OperationCounters` with a counter | in-process |
|
|
| AMB DocumentationLinks Base | Implemented | an asset has a link with a value | session |
|
|
| AMB DocumentationLinks Edit Base | Implemented | every asset has an editable link of at least 255 characters, and the store is persistent | session |
|
|
| AMB DocumentationLinks Edit Advanced | Implemented | every asset has AddLink and RemoveLink and room for two links of at least 255 characters, and the store is persistent | session |
|
|
| AMB Requirements | Implemented | an asset has a Requirements folder with an entry | session |
|
|
| AMB Capabilities | Implemented | an asset has a Capabilities folder with an entry | in-process |
|
|
| AMB Classification | Implemented | an asset has a `HasDictionaryEntry` reference to a dictionary entry object (OPC 10000-19) | session |
|
|
| AMB Current and Future Maintenance Activities | Implemented | every asset has a maintenance activity, and the condition type implements `IMaintenanceEventType` (`UseServerDefinedAlarmTypes`) | session |
|
|
| AMB Past Maintenance Activities | Not implemented yet (history) | - | - |
|
|
| AMB Local Time | Implemented | an asset has `0:LocalTime` | session |
|
|
| AMB Hierarchical Location Property | Implemented | an asset has the property | session |
|
|
| AMB Hierarchical Location Objects | Implemented | an asset is contained in a hierarchical location | session |
|
|
| AMB Operational Location Property | Implemented | an asset has the property | in-process |
|
|
| AMB Operational Location Objects | Implemented | an asset is contained in an operational location | in-process |
|
|
| AMB Digital Location | Implemented | an asset has the property | in-process |
|
|
| AMB Sub-assets | Implemented | an asset has another asset below it | session |
|
|
| AMB Asset relations | Implemented | an asset has a non-hierarchical reference to another asset | session |
|
|
|
|
The unit and facet names follow Tables 54 to 56 of the specification. The
|
|
profile database has no AMB entries yet (checked on 2026-10-01).
|
|
|
|
### Client
|
|
|
|
A client does not advertise conformance units; the table says which calls of
|
|
`AmbClient` serve the client units of OPC 10000-110 and the AMB Base Asset
|
|
Management Client Facet (`http://opcfoundation.org/UA-Profile/AMB/Client/BaseClient`),
|
|
whose only mandatory unit is AMB Client Asset Identification.
|
|
|
|
| Client conformance unit | Status | Calls |
|
|
| --- | --- | --- |
|
|
| AMB Client Asset Identification (mandatory) | Implemented | `ReadIdentificationAsync` |
|
|
| AMB Client Asset Discovery by ProductInstanceUri | Implemented | `FindAssetsAsync`, `EnumerateAssetsAsync` |
|
|
| AMB Client Asset Discovery by AssetId | Implemented | `FindAssetsAsync`, `EnumerateAssetsAsync` |
|
|
| AMB Client Asset Health Status | Implemented | `ReadDeviceHealthAsync`, `ReadHealthAlarmsAsync`, `ObserveHealthAlarmsAsync` |
|
|
| AMB Client Asset Health Tracking Status | Not implemented yet (history) | - |
|
|
| AMB Client Current and Future Maintenance Activities | Implemented | `ReadMaintenanceActivitiesAsync`, `ObserveMaintenanceAsync` |
|
|
| AMB Client Past Maintenance Activities | Not implemented yet (history) | - |
|
|
| AMB Client sub-assets | Implemented | `EnumerateSubAssetsAsync` |
|
|
| AMB Client sub-assets remote | Partly: an asset of another server is reported, not followed | `EnumerateSubAssetsAsync` |
|
|
|
|
## Client reference
|
|
|
|
`AmbClient` is created with `session.AssetManagement(telemetry)` or through
|
|
`AddAssetManagementClient()` (an `AmbClientFactory` and a
|
|
`Func<CancellationToken, Task<AmbClient>>`). It registers the AMB data types
|
|
with the session.
|
|
|
|
For bounded interactive reads, opt into the strict constructor:
|
|
|
|
```csharp
|
|
var assets = new AmbClient(session, telemetry, new AmbClientOptions
|
|
{
|
|
MaximumBrowseReferences = 1024,
|
|
MaximumBrowsePages = 128
|
|
});
|
|
AssetIdentificationRecord identity = await assets.ReadIdentificationAsync(assetId, cancellationToken);
|
|
foreach (AssetReadValue field in identity.Values)
|
|
{
|
|
// IsAbsent identifies a missing optional browse path, not a failed value read.
|
|
Console.WriteLine($"{field.SourceNodeId}: {field.Value.StatusCode}");
|
|
}
|
|
```
|
|
|
|
Hosted clients can use the same immutable options:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddClient(options =>
|
|
{
|
|
options.ApplicationName = "AssetClient";
|
|
options.ApplicationUri = "urn:localhost:AssetClient";
|
|
})
|
|
.AddAssetManagementClient(new AmbClientOptions
|
|
{
|
|
MaximumBrowseReferences = 1024,
|
|
MaximumBrowsePages = 128
|
|
});
|
|
```
|
|
|
|
Both `AmbClientFactory.CreateAsync` and the registered
|
|
`Func<CancellationToken, Task<AmbClient>>` apply these limits. Alternatively,
|
|
register an `AmbClientOptions` singleton before resolving the factory, or pass
|
|
options directly to the three-argument `AmbClientFactory` constructor. Without
|
|
options, the existing factory and builder APIs retain tolerant behavior.
|
|
Invalid limits are rejected before the factory opens a session.
|
|
|
|
Strict traversal limits apply while collecting pages and walking the asset
|
|
graph. Reads are batched, and returned records expose source identity, browse
|
|
path and the original `DataValue` in `Values`. Non-Good and wrongly typed reads
|
|
throw `AmbValueReadException`, whose `ReadValue` retains the original evidence.
|
|
A missing optional path has `IsAbsent == true`; a present Good-null value does
|
|
not, including a typed null `ExtensionObject` in a structure field such as
|
|
`LocalTime` or `MaintenanceSupplier`. Scalar fields still require their expected
|
|
value type. The existing constructor retains its tolerant behavior.
|
|
|
|
Strict condition inspection requires `DeviceHealthAlarms` and does not start an
|
|
implicit condition-refresh subscription. The existing event-observation methods
|
|
retain their documented stream semantics and do not populate the read-specific
|
|
`Values` evidence. Transport quotas still bound incoming message/value sizes;
|
|
the traversal options are not additional byte quotas.
|
|
|
|
Event observation falls back to known maintenance classes when subtype browsing
|
|
is denied or reaches a traversal limit. An incomplete class set is not cached,
|
|
so a later strict address-space read retries discovery and reports its failure.
|
|
|
|
Category `NodeVersion` describes alias-set changes, not an asset-wide metadata
|
|
ETag. A caller preparing an edit must also revalidate current alias membership,
|
|
the exact edit target, permissions and relevant old value. The client does not
|
|
provide atomic compare-and-swap.
|
|
|
|
| Area | Calls |
|
|
| --- | --- |
|
|
| Discovery | `FindAssetsAsync(pattern, category)`, `EnumerateAssetsAsync(category)`, `DiscoverAssetsAsync`, `ReadNodeVersionAsync`, `ObserveAssetSetChangesAsync` |
|
|
| Identification | `ReadIdentificationAsync` (on the asset, then in `2:Identification`; empty values count as not set), `WriteAssetIdAsync` |
|
|
| Health | `ReadDeviceHealthAsync`, `ReadHealthAlarmsAsync`, `ObserveHealthAlarmsAsync`, `AcknowledgeAsync` |
|
|
| Maintenance | `ReadMaintenanceActivitiesAsync`, `ObserveMaintenanceAsync`, `AcknowledgeAsync` |
|
|
| Documentation links | `ReadDocumentationLinksAsync` (with `IsWritable` from the `UserAccessLevel` and `IsUserLink` from the `UserLink` Property), `AddDocumentationLinkAsync`, `RemoveDocumentationLinkAsync`, `WriteDocumentationLinkAsync` |
|
|
| Locations and structure | `BrowseLocationsAsync`, `ReadLocationsOfAssetAsync`, `ReadContextAsync`, `ReadEntriesAsync`, `EnumerateSubAssetsAsync`, `ReadRelationsAsync` |
|
|
| Snapshot | `ReadAssetAsync` |
|
|
|
|
The observe calls need an `IStreamingSubscription`; without one they use the
|
|
default streaming subscription of a `ManagedSession`. Both select the
|
|
conditions of the notifier (`ConditionType`) and tell maintenance activities
|
|
apart by their condition class - `MaintenanceConditionClassType` or a subtype,
|
|
read from the server once (§12.1) - so the health and the maintenance stream
|
|
each get their own events and one does not see the other's. The fields of the
|
|
AMB interfaces are selected with `BaseEventType`, since the Device Integration
|
|
types are only recommended. `ReadHealthAlarmsAsync` and
|
|
`ReadMaintenanceActivitiesAsync` read the conditions an asset lists in its
|
|
`2:DeviceHealthAlarms` folder, and without the folder ask for a condition
|
|
refresh (`ManagedSession` only), which reports the retained conditions of the
|
|
asset. A `RevisionCounter` of -1, the "not supported" of OPC 10000-100, is
|
|
read as not set.
|
|
|
|
## Samples
|
|
|
|
| Sample | Shows |
|
|
| --- | --- |
|
|
| [AmbServer](../samples/AMB/AmbServer) | A cooling pump and a flow sensor (DI devices) and a hydraulic press (Machinery machine) registered as assets, with every building block, file-system persistence, and a simulation of alarms and maintenance; the press switches into its Maintenance operation mode while it is serviced |
|
|
| [AmbClient](../samples/AMB/AmbClient) | Discovery, snapshots of every asset, the location hierarchies, AddLink and RemoveLink, writing an AssetId, and streams of health alarms and maintenance activities |
|
|
|
|
See [samples/AMB](../samples/AMB/README.md) for how to run them.
|
|
|
|
`AmbServer` accepts `--host`, `--port`, an absolute `--pki-root`,
|
|
`--autoaccept true|false` and an optional `--run-seconds` from 1 through 3600.
|
|
Ordinary standalone defaults and persistent state behavior are unchanged.
|
|
|
|
## Testing
|
|
|
|
```bash
|
|
dotnet test tests/Opc.Ua.AMB.Tests
|
|
```
|
|
|
|
| Fixture | Covers |
|
|
| --- | --- |
|
|
| `AmbModelTests`, `AmbContractsTests` | The generated model and the contracts |
|
|
| `AmbNodeManagerTests`, `AmbHostingTests`, `AmbCompositionTests` | The manager, hosting, composition with DI and Pumps |
|
|
| `AssetRegistrationTests`, `ConfigurableAssetIdTests`, `AssetConfigurationStoreTests` | Registration, identification and the stores |
|
|
| `AssetDiscoveryTests` | The alias categories, FindAlias, NodeVersion |
|
|
| `HealthAlarmTests`, `MaintenanceActivityTests`, `DocumentationLinksTests`, `AssetStructureTests` | The building blocks in-process |
|
|
| `Amb*SessionTests` | Each area over a real client session |
|
|
| `AmbClientTests` | The client without a server |
|
|
| `AmbClientServerE2eTests` | `AmbClient` against a hosted server, a restart with persistence, and a Machinery machine as an asset |
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Cause | Fix |
|
|
| --- | --- | --- |
|
|
| An event field of `PotentialRootCauses` or `MaintenanceState` is always null | The select clause names the interface (`IRootCauseIndicationType`, `IMaintenanceEventType`) as its type definition; a server checks it against the type hierarchy of the event, which an interface is not part of | Select with `BaseEventType` as the type definition, as `AmbClient` does; the DI types work only for conditions that derive from them |
|
|
| `0:Aliases/FindAlias` does not find assets | The AMB categories are served through a registry of the AMB manager, not the server-wide one | Call `FindAlias` on `Assets`, `AssetsByProductInstanceUri` or `AssetsByAssetId` |
|
|
| `BadInvalidState` when registering an asset with an alarm or a maintenance activity | The owner's address space is already built and its builder sealed | Register the asset from the owner's setup (see [When to register](#when-to-register)) |
|
|
| `BadConfigurationError`: the AMB node manager is not part of the server | `AddAssetManagement` is missing | Add it |
|
|
| `BadConfigurationError`: no Device Integration namespace | No DI owner is registered | Add `AddOpcUaDi`, `AddMachinery`, `AddPumps` or `AddScales` |
|
|
| `BadConfigurationError`: no ProductInstanceUri | The asset publishes none | Set it, or turn off `RequireProductInstanceUri` for an asset completed later |
|
|
| `BadConfigurationError` when registering an asset with `AllowUserLinks()` | The store keeps what users add in memory only (§10.5.3) | Use `UseFileSystemStores` or another persistent store |
|
|
| `BadUserAccessDenied` from `AddLink` or writing a link | Anonymous session, or `AuthorizeLinkEdit` refuses | Authenticate, or set `AuthorizeLinkEdit` |
|
|
| An added link or the AssetId is gone after a restart | Memory store, or a changed application identity | Use `UseFileSystemStores` and keep the ApplicationUri and the namespaces |
|
|
| "AMB Configurable Asset Identification" is not advertised | The memory store does not survive a restart | Use a persistent store |
|
|
| An observe call fails with `BadInvalidState` | No streaming subscription and no `ManagedSession` | Pass an `IStreamingSubscription` |
|
|
| A server shows no `0:Locations`, or `BrowseLocationsAsync` finds nothing | `0:Locations` came with UA 1.05.02, and a location exists only once an asset is put into it or it is defined | `AmbClient` browses from `HierarchicalLocations` and `OperationalLocations` directly; on the server use `LocatedIn` or `DefineLocationAsync` |
|
|
|
|
## Known limitations
|
|
|
|
| Topic | Behaviour | Section |
|
|
| --- | --- | --- |
|
|
| Source timestamp of `DeviceHealth` after a restart | The timestamp marks when the asset entered its state while the server runs; after a restart it is the time of the restart, since the state is not persisted | 9.2 ("should") |
|
|
| Optional members of a maintenance activity | Once published, a member keeps its property; `UpdateAsync` changes its value but does not remove it | 12.2 |
|
|
| `2:PatchIdentifiers` | `WithVersionInformation` does not create it; an asset that publishes it keeps it | 10.2 |
|
|
| Location objects | An asset is contained by the deepest level of each path it is put into, not by the levels above | 13.3.3.1 |
|
|
| Event history | "AMB Asset Health Tracking" and "AMB Past Maintenance Activities" need a historian and are left for a later change | Table 54 |
|
|
|
|
## Model provenance
|
|
|
|
`src/Opc.Ua.AMB/Model/Opc.Ua.AMB.NodeSet2.xml` is the unmodified OPC
|
|
Foundation publication from
|
|
[`UA-Nodeset/latest/AMB`](https://github.com/OPCFoundation/UA-Nodeset/tree/latest/AMB):
|
|
model version 1.01.1, published 2024-02-27, requiring UA 1.05.02. The
|
|
`RequiredModel` version is a lower bound, not a pin.
|
|
|
|
`Opc.Ua.AMB.Upstream.NodeIds.csv` is the identifier table of the publication,
|
|
vendored unchanged. `Opc.Ua.AMB.NodeIds.csv` is derived from it with
|
|
`tools/nodesets/derive-identifier-table.py` (92 rows, 25 symbols renamed to
|
|
the generator's naming, for example `Aliases_Assets` to `Assets` for the
|
|
instances below namespace-0 objects) and pins the symbol-to-NodeId mapping;
|
|
the generator fails the build when the two disagree.
|
|
|
|
## Generator gaps this model uncovered
|
|
|
|
AMB is the first companion model that instantiates namespace-0 types at the
|
|
top level (`AliasNameCategoryType` below `0:Aliases`). That showed one new
|
|
gap; two known ones apply as well:
|
|
|
|
- **No identifier constants for instance children the NodeSet adds itself.**
|
|
The `NodeVersion` properties of `AssetsByProductInstanceUri` and
|
|
`AssetsByAssetId` are generated correctly, but get no `VariableIds`
|
|
constants, because the generator takes children of a top-level instance only
|
|
when the type declares them as mandatory. The server finds the property
|
|
through its browse name, the client through a browse path.
|
|
`AmbModelTests.NodeVersionHasNoIdentifierConstant` records the gap and fails
|
|
once the generator emits the constants. The obvious fix changes the
|
|
identifiers of other generated models, among them the reference server's,
|
|
and is left for a separate change.
|
|
- **A child created through `CreateChild` is invisible to clients.** As
|
|
described for [Pumps](Pumps.md#generator-gap-this-model-uncovered), the
|
|
generic path creates a child without reference type, type definition and
|
|
data type. When the AMB manager adds a `2:AssetId` through the slot of a
|
|
device type, it completes the property the way the generated helpers do.
|
|
- **The generated maintenance state machine has no transition tables.** Like
|
|
the Machinery state machines, `MaintenanceEventStateMachineState` does not
|
|
override `StateTable`, `TransitionTable` and `TransitionMappings`. The server
|
|
drives it with tables built from the generated
|
|
`MaintenanceEventStateMachineTypeIds`.
|
|
|
|
## See also
|
|
|
|
- [Device Integration developer guide](DeviceIntegration.md): `DiNodeManager`,
|
|
`ConfigureDevicesFor`, identification and the alarm types AMB builds on
|
|
- [Alias names](AliasNames.md): the Part 17 categories AMB discovery uses
|
|
- [Machinery developer guide](Machinery.md): the machine builders; a machine
|
|
becomes an asset through `IMachineHandle<T>.AsNode()`
|
|
- [OPC 10000-110 on the OPC Foundation reference site](https://reference.opcfoundation.org/specs/OPC-10000-110)
|