## Summary
### Documentation
- Stage the documentation index as a learning path: a first application,
independent client, server, and PubSub paths, then data and services,
companion models, production, advanced integration, and draft or
experimental capabilities.
- Add Getting started (repository samples or NuGet packages), OPC UA
concepts, Your first information model, and a production readiness
checklist.
- Add Native AOT application publishing, a first PubSub publisher and
subscriber exchange, and the UDP transport options.
- Add or expand contents navigation throughout `docs/`, and keep
contributor documentation separate.
- Correct outdated SDK examples and capability guidance, consolidate
repeated explanations into canonical guides, and repair local links and
heading structure.
- Rewrite What's New in 2.0 as a concise feature overview with short
linked details; keep migration instructions in the Migration Guide.
- Expand the feature catalogue with specification IDs, GDS, LDS, the OPC
10000-21 onboarding registrar, ISA-95, xRegistry, OpenUSD, and
sample-local models; distinguish draft status, explain crypto
pluggability, and bring performance guidance forward.
- Address review feedback on wording, active monitored items during
shadow reload, and the `Samples.md` filename and references.
- Regenerate the `opcua-v20-migration` plugin's bundled copy of the
migration guides so it matches the edited `docs/migrate/2.0.x` guides.
- Merge the latest `master`, preserving upstream resource-isolation
guidance within the task-based documentation index.
- Retain deferred Lingity findings where appropriate; this PR does not
claim that every document meets the Lingity score thresholds.
### Fixes found while verifying the guides
Each fix has regression tests that fail without it.
- **Client:** V2 subscriptions compared their lifetime with the
operation timeout instead of the session timeout, and logged a spurious
warning. `ISubscriptionEngineContext`, which is new in 2.0, gains a
`SessionTimeout` property; implementations must add it.
- **PubSub:** a subscriber now registers the `DataSetMetaData`
configured on its readers (OPC 10000-14 6.2.9.4), and the UADP decoder
decodes DataSetMessages that carry no ConfigurationVersion. Before, a
secured subscriber of the reference sample received no DataSets.
- **Source generator:** a ModelDesign method that declares its arguments
inline, without a method type, produced code that did not compile; it
now uses `MethodState`. The generated code of existing models is
unchanged.
- **MinimalClient sample:** waits for its first data-change notification
before it disconnects.
### Known issues, not addressed here
- UDP multicast: the publisher does not send on the interface that the
subscriber joins, and `MulticastLoopback` defaults to false. The PubSub
guide uses unicast for the exchange on one computer.
- The UDP metadata announcement is sent unsecured on secured writer
groups; a secured subscriber drops it with a warning.
- A server does not start in globalization-invariant mode, because
`ResourceManager` creates `CultureInfo("en-US")`. The Native AOT guide
cautions against the mode.
## Validation
- `git diff --check` and
`./.azurepipelines/validate-migration-plugin.ps1` (15 bundled migration
docs validated, plugin manifests synchronized).
- Local Markdown links, anchors, file-link casing, and contents lists
checked across all 87 documents under `docs/`. The index links every
top-level guide and every target of the previous index.
- Both Getting started routes, Your first information model, the PubSub
exchange, and the Native AOT publish of the Getting started client and
server were run as documented. The NuGet route and the tutorial use the
public `2.0.0-preview.5` packages.
- The changed libraries build for all target frameworks in Release
without warnings.
- Tests on .NET 10: Opc.Ua.SourceGeneration.Tests 199 passed;
Opc.Ua.SourceGeneration.Core.Tests 3,977 passed, 8 skipped;
Opc.Ua.Server.Tests `Fluent` category 621 passed; Opc.Ua.Client.Tests
subscription-engine tests 70 passed; Opc.Ua.PubSub.Tests 1,374 passed, 2
skipped; PubSub fuzz replay 145 passed. The new and affected tests also
pass on .NET Framework 4.8.
- Earlier merge validation: 174 resource-isolation tests passed on .NET
10.
- The full solution suite was not run locally.
Review and validation artifacts are maintained outside the repository.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
26 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) |
| 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
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