# Description Implement the released [OPC 40100-1](https://reference.opcfoundation.org/specs/OPC-40100-1) and [OPC 40100-2](https://reference.opcfoundation.org/specs/OPC-40100-2) specifications with generated models, provider-backed runtimes, typed clients, MCP workflows, and DI/Machinery integration. - **Part 1 stays in `Opc.Ua.Vision`** and its client/server packages alongside the distinct Perception model. Provide control/acquisition state machines, jobs and synchronized steps, recipe/product/configuration/result management, file transfers, diagnostics, events, and conditions. - **Part 2 is in `Opc.Ua.MachineVision`, `.Client`, and `.Server`.** Provide asset categories, identification, image-handling aspects, health/maintenance data, topology, and transactional updates. The base Vision assemblies do not reference these optional packages; the Part 2 wire model does not depend on Part 1. - Preserve the release-pinned model bytes, wire namespaces and NodeIds. Perception types are in `Opc.Ua.Vision.Perception`; the AMCM generated namespace remains `Opc.Ua.Vision.MachineVision.AMCM`, and asset client/server extension namespaces remain `Opc.Ua.Vision.Client` and `Opc.Ua.Vision.Server`. - Use `AddOpcUaMachineVision` and `AddOpcUaMachineVisionAMCM` for model loading and factory registration. Each has node-collection, encodeable-factory-builder and node-state-factory-builder overloads. These short entry points delegate to the generated implementations; existing namespace-derived generated names remain available for compatibility. - Support multiple independent named systems/assets under one namespace-owning manager. Reuse injectable providers, file binding, bounded memory stores, optional transactional `IFileSystemProvider` persistence, state machines, telemetry and conformance infrastructure. - Keep one `docs/Vision.md` guide, titled `Machine Vision (OPC 40100-1 and OPC 40100-2)`, and one `Opc.Ua.Mcp.Vision` package. Include deterministic server/client samples. - Expose 17 Machine Vision MCP tools, including capabilities/status, bounded relationships and observation, complete small-file uploads/downloads, management, explicit control, conditions, and chunked transfers. Catalogue counts: 39 Vision-specific, 43 with connection tools, 81 Vision+Robotics, 155 Full. Every Machine Vision wire parameter has a schema description. **Compatibility and limits:** `AddVision()` remains Perception-only unless released parts are selected. Standalone Part 2 uses `AddVision().AddMachineVision()`; Machinery composition uses `AddVisionToMachinery()`. Existing tool defaults and command signatures are preserved. Observation owns and cleans up its subscription: 5 seconds/50 notifications by default, capped at 60 seconds/100 notifications. Complete-file tools are capped at 1 MiB; larger files use chunked transfers. Domain Error values remain distinct from UA service failures. **Versioning:** the seven Vision/MachineVision/MCP packages and their Debug variants follow the repository root version instead of being forced to preview. Vision.OpenUsd and its descendants remain preview-only. Development builds still inherit the root prerelease version. No release version, tag, release branch, publication policy, or feed promotion is changed. **Review actions:** all fourteen review threads have action/evidence replies and are resolved. Changes include the unified guide, Perception namespace, separate Part 2 packages and final naming, multi-system ownership explanation, interrupted-write cleanup, observer failure isolation, complete MCP parameter descriptions, and explicit package inventory. The preview-layout migration topic and the requested ReleaseProcess caveat were removed. The test Publish collector now retries recognized transient failures within one cancellation budget and propagates other exceptions. Commit69b1f5db9applies the requested short loader names and renames standalone hosting to `AddMachineVision`, with callers, tests and documentation updated. The intervening master merge is preserved. ### Verification **Latest API naming changes, verified locally:** | Scope | Result | | --- | --- | | Full Vision suite, net10 | 572 passed, 1 existing skip, 0 failed | | Full Vision suite, net48 | 515 passed, 0 skipped, 0 failed | | Final loader/hosting cases after cross-TFM null-guard adjustment | 9 passed, 0 failed, 0 skipped | | Machine Vision MCP and profile cases | 198 passed, 0 failed, 0 skipped | | Main and MCP AOT test-project compilation | Both built with 0 warnings/errors | | MCP AOT companion, managed execution | 5 passed, 0 failed, 0 skipped; not claimed as native execution | `ModelLoaderNamesRegisterTypedFactories`, `ModelLoaderNamesRejectNullArguments`, and `GeneratedModelsLoadTogetherAndCreateTypedInstances` cover all six loader overloads, fluent return identities, typed instances, namespace coexistence and null guards. Existing standalone Part 2 tests verify idempotent registration, one-owner enforcement and null handling through `AddMachineVision`. No source-generator implementation, CLR namespace, wire identity or model payload was changed for these naming updates. The current69b1f5db9revision is running [CI](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826937), [package validation](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165823359), [MCP usability](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826874), [CodeQL](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826906), and [Images CI](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826884). These runs are not claimed as completed results. **Earlier completed verification:** all seven default-TFM package archives were inspected after the package rename: exact IDs, 27 clean DLLs, correct dependencies/readmes/licenses, and unchanged pinned model hashes. [CI onc75ec4fa6](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38142519760) passed, including the formerly failing Linux batch and required summary. Its unchanged coverage gate passed at **88.56% lines, 78.96% branches, 84.28% patch** against the 75% patch floor. The later bounded-Publish-helper regressions reproduced 13 failures before the fix and passed afterward; the full mainline triggering fixture passed 67 tests on each of net10/net48. These are historical results, not current full-matrix results. <details> <summary>Executed API naming validation commands and outcomes</summary> Run from the worktree root using the established artifact trees. Dependency builds were performed before the no-build full net10 run; the focused loader/hosting selection was rebuilt and rerun after the final null-guard adjustment. ```powershell $files = 'C:\Users\mschier\.copilot\session-state\deb2ba10-b0e9-4a44-8783-8331bc850814\files' $env:DOTNET_CLI_USE_MSBUILD_SERVER = '0' $env:TEMP = 'C:\Temp\mv-deb2\followup' $env:TMP = $env:TEMP # Passed: 572; Failed: 0; Skipped: 1. dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Release -f net10.0 --no-build --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-net10" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup' # Passed: 9; Failed: 0; Skipped: 0, on the final source. dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Release -f net10.0 --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --filter 'FullyQualifiedName~MachineVisionModelTests|FullyQualifiedName~StandalonePartTwoRegistrationUsesOneOwnerAndOneModelProvider|FullyQualifiedName~StandalonePartTwoRequiresRegistrationAndRejectsCompetingOwnership' --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-final-model" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup' # Passed: 198; Failed: 0; Skipped: 0. dotnet test tests\Opc.Ua.Tools.Tests\Opc.Ua.Tools.Tests.csproj -c Release -f net10.0 --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --filter 'FullyQualifiedName~VisionMachineToolsTests|FullyQualifiedName~McpHostBuilderTests|FullyQualifiedName~McpToolProfileCompositionTests' --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-final-mcp" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup' $env:TEMP = 'C:\Temp\mv-deb2\followup48' $env:TMP = $env:TEMP # Passed: 515; Failed: 0; Skipped: 0. dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Debug -f net48 --no-restore --artifacts-path "$files\solution-net48-artifacts" -p:CustomTestTarget=net48 -p:UseSharedCompilation=false -m:1 -nr:false --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-net48-final" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup48' ``` Opt-in long-running/stress/durable tiers have not been rerun locally; the unqualified **all tests** checklist item remains open. Original implementation matrix counts are not presented as current results. </details> ## Related Issues - Fixes #4732. The requester reviewed and approved the implementation plan and subsequent packaging/API decisions in the development session. The tracking issue records the design; maintainer architectural discussion/sign-off there is still pending. This PR remains a draft. ## 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. - [x] 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.
27 KiB
Alarms and Conditions (OPC UA Part 9)
This guide covers the stack's OPC UA Part 9 Alarms & Conditions support. It explains how to build alarm-aware servers and consume alarm events on clients. It also introduces the helpers for latched alarms, alarm groups, suppression, alarm metrics, and streaming alarm records.
For the formal model, see OPC UA Part 9 — Alarms and Conditions.
Quick reference
| Concern | Server entry point | Client entry point |
|---|---|---|
| Create an alarm | new AlarmConditionState(telemetry, parent); alarm.Create(...) |
n/a |
| Acknowledge / Confirm | AcknowledgeableConditionState.OnAcknowledgeCalled (auto-wired) |
AlarmClient.AcknowledgeAsync / ConfirmAsync |
| Silence | AlarmConditionState.SetSilenceState |
AlarmClient.SilenceAsync |
| Suppress / Unsuppress | SetSuppressedState |
AlarmClient.SuppressAsync / UnsuppressAsync |
| Out-of-service | SetOutOfServiceState |
AlarmClient.RemoveFromServiceAsync / PlaceInServiceAsync |
| Latched + Reset | SetLatchedState + auto from SetActiveState |
AlarmClient.ResetAsync |
| Shelve / Unshelve | SetShelvingState |
AlarmClient.TimedShelveAsync / OneShotShelveAsync / UnshelveAsync |
| Re-alarm | ProcessReAlarm |
n/a (server timer) |
| Filtered retain | ConditionState.SupportsFilteredRetain |
n/a (per-subscription) |
| Additional retention | ConditionState.RetainStateProvider |
read Retain / observe condition events |
| Alarm groups | AlarmGroup, AlarmSuppressionEngine |
AlarmClient.GetGroupMembershipsAsync |
| Alarm rate | AlarmRateTracker |
read AlarmMetricsType attributes |
| Refresh state | Server.ConditionRefresh (server-driven) |
AlarmClient.ConditionRefreshAsync / ConditionRefresh2Async |
| Stream alarm events | n/a | IStreamingSubscription.SubscribeAlarmsAsync |
| Decode raw event fields | n/a | EventRecordDecoderRegistry.Default.Decode |
| Build an event filter | n/a | {Type}Record.EventFilters.Build(registry?) |
Create AlarmClient from any ISession and a telemetry context. It delegates
each Part 9 method call to the matching source-generated *TypeClient proxy.
The generated proxy is the single source for each method NodeId:
AlarmClient alarms = session.GetAlarmClient(telemetry);
Dependency-injected hosts can call the
builder.AddAlarms()
extension and resolve AlarmClientFactory. The factory passes the host's
telemetry context to the client:
var factory = sp.GetRequiredService<AlarmClientFactory>();
AlarmClient alarms = factory.Create(session);
If you already have a typed handle to a specific condition node you
can also construct a proxy directly — AlarmClient is a convenience
façade over these:
await new AlarmConditionTypeClient(session, conditionId, telemetry)
.AcknowledgeAsync(eventId, comment);
Server side
An enabled condition can supply RetainStateProvider for an additional
application retention requirement, such as an unresolved Machine Vision error
that must remain visible after acknowledgement. Returning false does not bypass
standard acknowledgement, confirmation, or branch retention. Disabled conditions
are not retained by this provider. The callback must be non-blocking and receives
the branch being evaluated when the condition is branched.
Fluent-created alarms have distinct per-instance NodeIds for their entire
subtree, including condition Methods and state properties. Configuring
WithLimits materializes the standard typed limit properties, assigns
instance identifiers, and registers them for browse/read access. Repeated
limit configuration updates the same property in place; omitted limits
remain absent.
Creating an alarm
AlarmConditionState is the central server-side state type for all
Part 9 alarms. It is source-generated from the standard NodeSet and
extended with hand-written behavior. The behavior partial file
(src/Opc.Ua.Core.Types/State/AlarmConditionState.Methods.cs) wires
every Part 9 method handler during OnAfterCreate, so as soon as you
populate the optional state nodes the corresponding methods are
callable.
var alarm = new AlarmConditionState(telemetry, parent);
alarm.Create(
context,
nodeId: NodeId.Null, // server-assigned
browseName: new QualifiedName("MyAlarm"),
displayName: null,
assignNodeIds: true);
alarm.SetEnableState(context, enabled: true);
alarm.SetSeverity(context, EventSeverity.High);
alarm.SetActiveState(context, active: true);
alarm.ReportEvent(context, alarm);
Before calling Create, assign any optional state nodes to properties such as
alarm.SilenceState, alarm.OutOfServiceState, and alarm.LatchedState.
The quickstart reference server creates these nodes through
AlarmConditionTypeHolder.Initialize in
samples/Quickstarts.Servers/Alarms/AlarmHolders/.
Driving state from your process
All state transitions go through typed setters that:
- update the
TwoStateVariableStatevalue +Id - stamp
TransitionTime - recompute
EffectiveDisplayNameand the compositeSuppressedOrShelvedflag - clear
ChangeMasksso the next publish cycle sees the new state
| Setter | Notes |
|---|---|
SetEnableState(context, enabled) |
Inherited from ConditionState. Disabling clears Retain per Part 9 §5.5.2. |
SetSeverity(context, severity) |
Records LastSeverity before updating. |
SetActiveState(context, active) |
On true: if LatchedState is present, sets it true; if SilenceState is present and silenced, clears it. On false: if shelved as OneShotShelve, unshelves. |
SetSuppressedState(context, suppressed) |
Updates SuppressedOrShelved taking OutOfServiceState and ShelvingState into account. |
SetOutOfServiceState(context, outOfService) |
Sets SuppressedOrShelved true when out of service (Part 9 §5.8.2). |
SetShelvingState(context, shelved, oneShot, shelvingTime) |
Drives the ShelvedStateMachineState, runs the unshelve timer, computes UnshelveTime. |
SetLatchedState(context, latched) |
Direct latched-state setter; usually called automatically from SetActiveState. |
SetSilenceState(context, silenced) |
Direct silence-state setter; usually called automatically on activation / re-alarm. |
Latched alarms
If you populate alarm.LatchedState, the alarm becomes a latching
alarm (Part 9 §4.8). The semantics:
SetActiveState(context, true)— also setsLatchedState = true.SetActiveState(context, false)—ActiveStatereflects the real process state, butLatchedStatestaystrue.Reset(server-side method, auto-wired) — clearsLatchedState.Resetaccepts the request only when the alarm is enabled, inactive, and acknowledged, and when it is confirmed ifConfirmedStateis present. Otherwise, it returnsBad_InvalidState.
The server retains latched alarms (Retain = true) while LatchedState.Id
is true, so clients can see them during a refresh.
Re-alarming
A re-alarm reminder fires when an alarm has been active and
unacknowledged longer than ReAlarmTime. The state type provides a
helper rather than an automatic timer (so the host owns scheduling and
event generation):
// In your re-alarm scheduler (e.g. an external Timer)
if (alarm.IsReAlarmEnabled && alarm.ActiveState.Id.Value
&& alarm.AckedState?.Id.Value != true)
{
alarm.ProcessReAlarm(context);
}
// On deactivation / acknowledge:
alarm.ResetReAlarmRepeatCount(context);
ProcessReAlarm performs these actions:
- clears
AckedState(forces re-acknowledgement) - clears
SilenceState(audible annunciation resumes) - increments
ReAlarmRepeatCount - calls
ReportStateChangeto publish the new event
Audible alarms and silencing
When AudibleEnabled = true and AudibleSound has a value,
UpdateAudibleState clears the silence state on activation. The next
activation is then audible:
ByteString sound = LoadWavFile();
alarm.UpdateAudibleState(context, active: true, soundData: sound);
SilenceAsync from the client (or the Silence method handler on the
server) sets SilenceState.Id = true.
Suppression, out-of-service, shelving
All three states contribute to the SuppressedOrShelved boolean. The
state-type setters keep the flag in sync. Clearing one state does not
clear SuppressedOrShelved while another state remains active:
alarm.SetSuppressedState(context, true); // SuppressedOrShelved = true
alarm.SetOutOfServiceState(context, true); // SuppressedOrShelved stays true
alarm.SetSuppressedState(context, false); // SuppressedOrShelved stays true
alarm.SetOutOfServiceState(context, false); // SuppressedOrShelved = false
Filtered retain
Part 9 B.1.4 lets a server set a client-specific Retain flag based on
that client's event filter. When a condition no longer matches a client's
where clause, the server sends that client a final event with
Retain = false. The alarm remains active for everyone else.
Opt a condition in by setting SupportsFilteredRetain:
alarm.SupportsFilteredRetain =
PropertyState<bool>.With<VariantBuilder>(alarm, value: true);
Notes on the shape of that property:
- Part 9 provides
SupportsFilteredRetainon the ConditionType only — the standard nodeset declares no modelling rule for it, so condition instances do not carry it as a child. The C# property is therefore the server side switch, not an address space node:FindChild(BrowseNames.SupportsFilteredRetain)on an instance returns null by design. Clients read the flag from the type node, which the generated address space already builds. - Branches inherit the parent's setting —
CreateBranchcopies it, since nothing that walks the instance children would. - Each condition and branch is tracked separately, so one branch leaving filter scope does not affect its siblings or its parent.
ConditionRefreshreevaluates refreshed conditions against the where clause and primes the tracking again.- Changing a monitored item's where clause with
ModifyMonitoredItemsdiscards tracking for the previous filter. Durable subscriptions preserve tracking across a restart. - Each client receives a trailing event with
Retain = false(Part 9, 5.5.2, Figure 11), regardless of the server'sRetainvalue. Otherwise, the client would keep an alarm that it should drop. The override applies only to that client's event fields; it leaves the shared event snapshot unchanged. Other clients whose filters still include the condition keep receiving the server's actual value.
Alarm groups and first-in-group
src/Opc.Ua.Server/Alarms/AlarmGroup.cs wraps a generated
AlarmGroupState and provides typed add/remove/enumerate:
var group = new AlarmGroup(motorAlarmGroupState);
group.AddMember(motorHighTempAlarm);
group.AddMember(motorLowOilAlarm);
foreach (NodeId id in group.GetMemberIds(context))
{
// ...
}
AlarmSuppressionEngine centralizes both the AlarmSuppressionGroup
pattern and the FirstInGroup pattern. Register on startup, call
Evaluate from your simulation/process loop, and the engine routes
suppression to the right alarm members:
using var engine = new AlarmSuppressionEngine();
engine.RegisterSuppressionGroup(
suppressionGroup: motorShutdownGroup,
suppressionSource: () => motorIsShutDown.Value,
alarmMembers: new[] { motorHighTempAlarm, motorLowOilAlarm });
engine.RegisterFirstInGroupAlarm(
firstAlarm: masterTripAlarm,
group: tripGroup,
otherMembers: dependentTripAlarms);
// In your periodic update:
engine.Evaluate(context);
// On master trip activation:
engine.OnFirstInGroupActiveChanged(context, masterTripAlarm, tripGroup,
firstActive: true);
The first Evaluate call applies the current state. Clients therefore see
a coherent suppression state immediately after registration; no state
transition is required.
Live demo: The reference implementation in
samples/Quickstarts.Servers/Alarms/AlarmNodeManager.csexposes an/Alarms/AnalogGroup(AlarmGroupType) containing every analog-source alarm and a writable/Alarms/MaintenanceModeboolean. WritingtruetoMaintenanceModerunsAlarmSuppressionEngine.Evaluate(...)and suppresses every group member. Writingfalseclears suppression on the next evaluation. The reference node manager derives fromAsyncCustomNodeManager, so this file also demonstrates the modern async base class.
Alarm metrics (rate tracking)
AlarmRateTracker records activations into a sliding window and
exposes CurrentAlarmRate / MaximumAlarmRate suitable for surfacing
through an AlarmMetricsType instance:
var tracker = new AlarmRateTracker(TimeSpan.FromMinutes(1));
alarm.OnSilenceRequested = (ctx, a) =>
{
tracker.RecordActivation();
return ServiceResult.Good;
};
// Periodically push to AlarmMetrics:
metrics.CurrentAlarmRate.Value = tracker.CurrentAlarmRate;
metrics.MaximumAlarmRate.Value = tracker.MaximumAlarmRate;
Vetoing alarm operations
Each Part 9 alarm method has an optional delegate that runs before
the default state transition. Returning a Bad status from the
delegate aborts the operation; returning Good (or null) lets the
default behavior run:
| Delegate | Triggered by |
|---|---|
alarm.OnSilenceRequested |
Silence method |
alarm.OnSuppressRequested(suppressing: bool) |
Suppress / Unsuppress |
alarm.OnOutOfServiceRequested(outOfService: bool) |
RemoveFromService / PlaceInService |
alarm.OnResetRequested |
Reset (latched alarms) |
alarm.OnShelve |
OneShotShelve / TimedShelve / Unshelve (existing) |
alarm.OnResetRequested = (ctx, a) =>
{
if (!hardwareDiagnosticPassed)
{
return new ServiceResult(StatusCodes.BadUserAccessDenied,
"Hardware diagnostic must pass before reset.");
}
return ServiceResult.Good;
};
Audit events
Every Part 9 alarm method generates the spec-mandated audit event
type automatically. You do not have to call ReportEvent for the
audit event — it happens inside the method handler when
AreEventsMonitored is true:
| Method | Audit event type |
|---|---|
Silence |
AuditConditionSilenceEventType |
Suppress / Unsuppress / *2 |
AuditConditionSuppressionEventType |
RemoveFromService / PlaceInService / *2 |
AuditConditionOutOfServiceEventType |
Reset / Reset2 |
AuditConditionResetEventType |
Existing: Acknowledge / Confirm |
AuditConditionAcknowledgeEventType / AuditConditionConfirmEventType |
| Existing: shelving | AuditConditionShelvingEventType |
Client side
AlarmClient — typed operations
AlarmClient is the strongly-typed client API for Part 9 methods.
Each method delegates to the matching source-generated proxy:
ConditionTypeClient, AcknowledgeableConditionTypeClient,
AlarmConditionTypeClient, DialogConditionTypeClient, or
ShelvedStateMachineTypeClient. The proxy receives the caller-supplied
conditionId as its ObjectId, following the Part 9 §5.5.4 idiom that
accepts ConditionId as ObjectId. The generated proxy remains the single
source for each method NodeId and argument shape.
AlarmClient alarms = session.GetAlarmClient(telemetry);
// ConditionType methods
await alarms.EnableAsync(conditionId);
await alarms.DisableAsync(conditionId);
await alarms.AddCommentAsync(conditionId, eventId,
new LocalizedText("en", "Looks like flow sensor drift"));
await alarms.ConditionRefreshAsync(subscriptionId);
await alarms.ConditionRefresh2Async(subscriptionId, monitoredItemId);
// AcknowledgeableConditionType
await alarms.AcknowledgeAsync(conditionId, eventId,
new LocalizedText("en", "Operator review complete"));
await alarms.ConfirmAsync(conditionId, eventId,
new LocalizedText("en", "Maintenance verified"));
// AlarmConditionType
await alarms.SilenceAsync(conditionId);
await alarms.SuppressAsync(conditionId,
comment: new LocalizedText("en", "Routine maintenance"));
await alarms.UnsuppressAsync(conditionId);
await alarms.RemoveFromServiceAsync(conditionId);
await alarms.PlaceInServiceAsync(conditionId);
await alarms.ResetAsync(conditionId); // latched-alarm reset
await alarms.TimedShelveAsync(conditionId, shelvingTime: 30000); // 30s
await alarms.OneShotShelveAsync(conditionId);
await alarms.UnshelveAsync(conditionId);
ArrayOf<NodeId> groups = await alarms.GetGroupMembershipsAsync(conditionId);
The *Async(... comment ...) overloads call the spec-defined *2 method
when the comment is non-empty: Suppress2, Unsuppress2,
RemoveFromService2, PlaceInService2, and Reset2.
Subscribing to alarms with IAsyncEnumerable
Alarm events flow through the streaming subscription
API. The
AlarmStreamExtensions.SubscribeAlarmsAsync extension returns
strongly-typed records:
ManagedSession session = ...;
IStreamingSubscription streaming = session.DefaultStreaming;
await foreach (ConditionTypeRecord record in streaming
.SubscribeAlarmsAsync(notifierId: ObjectIds.Server, ct: ct)
.ConfigureAwait(false))
{
switch (record)
{
case ExclusiveLimitAlarmTypeRecord limit:
Console.WriteLine($"{limit.SourceName} limit-state-id={limit.LimitState}");
break;
case AlarmConditionTypeRecord alarm when alarm.ActiveStateId == true:
await alarms.AcknowledgeAsync(alarm.ConditionId,
alarm.EventId,
new LocalizedText("en", "Auto-ack")).ConfigureAwait(false);
break;
case DialogConditionTypeRecord dialog:
// Pick a response index from dialog.ResponseOptionSet
await alarms.RespondAsync(dialog.ConditionId,
selectedResponse: 0).ConfigureAwait(false);
break;
}
}
State-machine waits compose naturally with
TakeUntilAsync / WithTimeoutAsync:
// Wait for myAlarm to clear, or 5 minutes — whichever comes first.
await streaming.SubscribeAlarmsAsync(ObjectIds.Server)
.TakeUntilAsync(r =>
r is AlarmConditionTypeRecord a && a.ConditionId == myAlarmId &&
a.ActiveStateId == false)
.WithTimeoutAsync(TimeSpan.FromMinutes(5))
.LastAsync(ct);
Typed alarm records
Alarm and condition records are source-generated by the
Opc.Ua.SourceGeneration analyzer (see the EventRecordGenerator).
For every ObjectType derived from BaseEventType, the generator emits
a partial record {Type}Record. Each generated record derives from its
parent record and exposes one init-only property for each declared field.
The standard NodeSet produces the following hierarchy, abridged to show
the most commonly observed types:
EventRecord (anchor; hand-written)
└── BaseEventTypeRecord (i=2041)
└── ConditionTypeRecord (i=2782)
├── DialogConditionTypeRecord (i=2830)
└── AcknowledgeableConditionTypeRecord
└── AlarmConditionTypeRecord
├── LimitAlarmTypeRecord
│ ├── ExclusiveLimitAlarmTypeRecord
│ └── NonExclusiveLimitAlarmTypeRecord
├── DiscreteAlarmTypeRecord
│ └── OffNormalAlarmTypeRecord
│ └── CertificateExpirationAlarmTypeRecord
└── DiscrepancyAlarmTypeRecord
Vendor models that derive from these types automatically get their own
*TypeRecord, which derives from the closest standard ancestor. No
checked-in code or manual class definition is needed. Add a hand-written
partial record VibrationAlarmTypeRecord to your project to extend the
generated declaration with computed properties or custom helpers.
The decoder upgrades the record type based on which fields are
populated in the event. A simple switch on the record type gives you
the right field set:
EventRecord? record = EventRecordDecoderRegistry.Default.Decode(eventFields);
if (record is CertificateExpirationAlarmTypeRecord cert)
{
Console.WriteLine($"Cert {cert.CertificateType} expires {cert.ExpirationDate}");
}
The shared ConditionTypeRecord.ConditionId property is a hand-written
alias for SourceNode. Part 9 defines ConditionId as the NodeId of the
condition object that raised the event. The event reports this NodeId in
the SourceNode field.
Source-generated decoders + EventRecordDecoderRegistry
Every record emitted by EventRecordGenerator also exposes a nested
static class Decoder. The decoder provides a positional StandardFields
table and a Decode(IReadOnlyList<Variant>) method that populates the
record's own and inherited init-only properties. A per-file
Register{ModelPrefix}Decoders(this EventRecordDecoderRegistry) extension
registers each generated decoder with a caller-supplied registry.
The process-wide EventRecordDecoderRegistry.Default registers the
standard UA model. It routes by the event's EventType field. If the
exact type is not registered, it walks the OPC UA event-type hierarchy
through an optional SuperTypeResolver. Vendor models can register their
generated extensions with Register{Prefix}Decoders or
CreateChildScope().Register{Prefix}Decoders() for test isolation.
// EventType-keyed dispatch backed by source-generated decoders:
EventRecord? rec = EventRecordDecoderRegistry.Default.Decode(eventFields);
// Vendor scenario — register extra decoders on an app-scoped child:
var app = EventRecordDecoderRegistry.Default
.CreateChildScope()
.RegisterMyVendorDecoders();
EventRecord? vendorRec = app.Decode(eventFields);
Per-record event filters
Every generated {Type}Record exposes a nested
static class EventFilters alongside its Decoder block. The
Build(registry?) factory returns an EventFilter. Its where clause
restricts events to OfType({recordTypeId}), and its select clauses come
from the supplied registry's composed StandardFields (by default,
EventRecordDecoderRegistry.Default). Use the filter with
EventRecordDecoderRegistry.Decode. The registry remaps composed field
positions to each decoder's layout before decoding, so vendor models
extend transparently.
// Filter for alarm events:
EventFilter filter = AlarmConditionTypeRecord.EventFilters.Build();
// Or for any condition:
EventFilter f2 = ConditionTypeRecord.EventFilters.Build();
// Or for dialog events:
EventFilter f3 = DialogConditionTypeRecord.EventFilters.Build();
// Or for a specific subtype:
EventFilter f4 = CertificateExpirationAlarmTypeRecord.EventFilters.Build();
// Vendor scenario — pass a child registry so the filter superset
// includes the vendor model's fields:
var app = EventRecordDecoderRegistry.Default
.CreateChildScope()
.RegisterMyVendorDecoders();
EventFilter vendorFilter = VibrationAlarmTypeRecord.EventFilters.Build(app);
Pass the same registry to Subscribe*Async through its registry:
parameter. This ensures the streaming side uses the registry that built
the filter.
Dialog conditions
A DialogConditionType event arrives as a
DialogConditionTypeRecord. The Respond and Respond2 methods on
IDialogConditionOperations close out the dialog. The decoded record
exposes the prompt and available response option set so the caller
can pick an index:
await foreach (DialogConditionTypeRecord dialog in streaming.SubscribeDialogsAsync(notifierId))
{
Console.WriteLine($"Prompt: {dialog.Prompt}");
LocalizedText[] options = dialog.ResponseOptionSet ?? Array.Empty<LocalizedText>();
Console.WriteLine($"Options: {string.Join(", ", options)}");
// Pick whichever option matches your scenario.
int selectedIndex = 0;
await alarms.Respond2Async(dialog.ConditionId, selectedIndex,
new LocalizedText("en", "Approved by operator-1")).ConfigureAwait(false);
}
The server-side DialogConditionType properties OkResponse,
CancelResponse, and DefaultResponse carry canonical indices (Part 9
§5.6.2). Applications can read these indices separately through the Read
service. The standard DialogConditionTypeRecord does not include them;
it contains only the dialog prompt and active state.
ConditionRefresh
Both Part 9 refresh methods are available:
// Refresh all conditions for the subscription
await alarms.ConditionRefreshAsync(subscriptionId);
// Refresh just one monitored item's conditions (Part 9 §5.5.8)
await alarms.ConditionRefresh2Async(subscriptionId, monitoredItemId);
The streaming subscription also provides
ISubscription.ConditionRefreshAsync. Both refresh methods are
equivalent when AlarmClient and IStreamingSubscription use the same
session.
Reference
- OPC UA Part 9 — Alarms and Conditions
- IEC 62682 — Management of alarm systems for the process industries
- ISA 18.2 — Management of Alarm Systems for the Process Industries
- Subscriptions and Monitored Items Service Set —
IStreamingSubscriptionand the V2 subscription engine - State Machines — generic Part 16 state-machine API used by
AlarmClient.GetShelvingStateAsync/ObserveShelvingTransitionsAsync - Model Change Tracking — client cache invalidation on address-space changes
- Source:
src/Opc.Ua.Server/Alarms/,src/Opc.Ua.Client/Alarms/,src/Opc.Ua.Core.Types/State/AlarmConditionState.Methods.cs - Reference client sample:
samples/Reference/ConsoleReferenceClient/AlarmClientSample.cs - Conformance tests:
tests/Opc.Ua.History.Tests/AlarmsAndConditions*.cs