mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
598 lines
24 KiB
Markdown
598 lines
24 KiB
Markdown
# Alarms and Conditions (OPC UA Part 9)
|
|
|
|
This guide describes the OPC UA Part 9 *Alarms & Conditions* support in
|
|
this stack: how to build alarm-aware servers, how to consume alarm events
|
|
on the client, and how the new helpers (latched alarms, alarm groups,
|
|
suppression engine, alarm metrics, streaming alarm records) fit together.
|
|
|
|
For the formal model, see
|
|
[OPC UA Part 9 — Alarms and Conditions](https://reference.opcfoundation.org/specs/OPC-10000-9/full).
|
|
|
|
- [Quick reference](#quick-reference)
|
|
- [Server side](#server-side)
|
|
- [Creating an alarm](#creating-an-alarm)
|
|
- [Driving state from your process](#driving-state-from-your-process)
|
|
- [Latched alarms](#latched-alarms)
|
|
- [Re-alarming](#re-alarming)
|
|
- [Audible alarms and silencing](#audible-alarms-and-silencing)
|
|
- [Suppression, out-of-service, shelving](#suppression-out-of-service-shelving)
|
|
- [Alarm groups and first-in-group](#alarm-groups-and-first-in-group)
|
|
- [Alarm metrics (rate tracking)](#alarm-metrics-rate-tracking)
|
|
- [Vetoing alarm operations](#vetoing-alarm-operations)
|
|
- [Audit events](#audit-events)
|
|
- [Client side](#client-side)
|
|
- [`AlarmClient` — typed operations](#alarmclient--typed-operations)
|
|
- [Subscribing to alarms with `IAsyncEnumerable`](#subscribing-to-alarms-with-iasyncenumerable)
|
|
- [Typed alarm records](#typed-alarm-records)
|
|
- [Per-record event filters](#per-record-event-filters)
|
|
- [Dialog conditions](#dialog-conditions)
|
|
- [`ConditionRefresh`](#conditionrefresh)
|
|
- [Reference](#reference)
|
|
|
|
## 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) |
|
|
| 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?)` |
|
|
|
|
`AlarmClient` is obtained from any `ISession` and a telemetry context;
|
|
internally it delegates every Part 9 method call to the matching
|
|
source-generated `*TypeClient` proxy (so there is exactly one place
|
|
that knows each method NodeId — the generator):
|
|
|
|
```csharp
|
|
AlarmClient alarms = session.GetAlarmClient(telemetry);
|
|
```
|
|
|
|
For dependency-injected hosts use the
|
|
[`builder.AddAlarms()`](DependencyInjection.md#alarms-and-conditions)
|
|
extension and resolve `AlarmClientFactory` (which threads the
|
|
host's telemetry into the client for you):
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
await new AlarmConditionTypeClient(session, conditionId, telemetry)
|
|
.AcknowledgeAsync(eventId, comment);
|
|
```
|
|
|
|
## Server side
|
|
|
|
### 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.
|
|
|
|
```csharp
|
|
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);
|
|
```
|
|
|
|
The optional state nodes are created the way you would expect on a
|
|
generated `AlarmConditionState`: assign to `alarm.SilenceState`,
|
|
`alarm.OutOfServiceState`, `alarm.LatchedState`, etc. before calling
|
|
`Create`. The quickstart reference server creates these via
|
|
`AlarmConditionTypeHolder.Initialize` —
|
|
`samples/Quickstarts.Servers/Alarms/AlarmHolders/`.
|
|
|
|
### Driving state from your process
|
|
|
|
All state transitions go through typed setters that:
|
|
|
|
- update the `TwoStateVariableState` value + `Id`
|
|
- stamp `TransitionTime`
|
|
- recompute `EffectiveDisplayName` and the composite `SuppressedOrShelved` flag
|
|
- clear `ChangeMasks` so 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 sets `LatchedState = true`.
|
|
- `SetActiveState(context, false)` — `ActiveState` reflects the real
|
|
process state, but `LatchedState` stays `true`.
|
|
- `Reset` (server-side method, auto-wired) — clears `LatchedState`.
|
|
The `Reset` method validates **all** preconditions before accepting:
|
|
enabled, not active, acknowledged, confirmed (if `ConfirmedState` is
|
|
present). Any other state returns `Bad_InvalidState`.
|
|
|
|
Latched alarms are *retained* (`Retain = true`) for as long as
|
|
`LatchedState.Id` is `true`, so a client refresh sees them.
|
|
|
|
### 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):
|
|
|
|
```csharp
|
|
// 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`:
|
|
|
|
- clears `AckedState` (forces re-acknowledgement)
|
|
- clears `SilenceState` (audible annunciation resumes)
|
|
- increments `ReAlarmRepeatCount`
|
|
- calls `ReportStateChange` so the new event is published
|
|
|
|
### Audible alarms and silencing
|
|
|
|
If `AudibleEnabled = true` and `AudibleSound` is populated, the
|
|
`UpdateAudibleState` helper takes care of clearing the silence state
|
|
when the alarm activates (so the next activation is audible again):
|
|
|
|
```csharp
|
|
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 that flag in sync — clearing one does **not**
|
|
clear `SuppressedOrShelved` while the others are still active:
|
|
|
|
```csharp
|
|
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
|
|
```
|
|
|
|
### Alarm groups and first-in-group
|
|
|
|
`src/Opc.Ua.Server/Alarms/AlarmGroup.cs` wraps a generated
|
|
`AlarmGroupState` and provides typed add/remove/enumerate:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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 always applies the current state, so
|
|
clients see a coherent suppression state immediately after
|
|
registration — there is no edge required.
|
|
|
|
> **Live demo:** The `samples/Quickstarts.Servers/Alarms/
|
|
> AlarmNodeManager.cs` reference implementation wires this up
|
|
> end-to-end. It exposes a `/Alarms/AnalogGroup` (`AlarmGroupType`)
|
|
> containing every analog-source alarm and a writable
|
|
> `/Alarms/MaintenanceMode` boolean. Writing `true` to
|
|
> MaintenanceMode runs `AlarmSuppressionEngine.Evaluate(...)` and
|
|
> suppresses every group member; writing `false` clears
|
|
> suppression on the next evaluation. The reference server's node
|
|
> manager itself derives from `AsyncCustomNodeManager`, so the same
|
|
> file is a worked example of porting a Part 9 demo to 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:
|
|
|
|
```csharp
|
|
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) |
|
|
|
|
```csharp
|
|
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
|
|
`*TypeClient` proxy (`ConditionTypeClient`,
|
|
`AcknowledgeableConditionTypeClient`,
|
|
`AlarmConditionTypeClient`, `DialogConditionTypeClient`,
|
|
`ShelvedStateMachineTypeClient`), passing the caller-supplied
|
|
`conditionId` as the proxy's `ObjectId`. This honours the Part 9
|
|
§5.5.4 idiom (`ConditionId` is acceptable as `ObjectId`) and keeps
|
|
exactly one source of truth — the generated proxy — for every
|
|
method NodeId and argument shape.
|
|
|
|
```csharp
|
|
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 automatically pick the
|
|
spec-defined `*2` method when a non-empty comment is supplied
|
|
(`Suppress2`, `Unsuppress2`, `RemoveFromService2`, `PlaceInService2`,
|
|
`Reset2`).
|
|
|
|
### Subscribing to alarms with `IAsyncEnumerable`
|
|
|
|
Alarm events flow through the [streaming subscription
|
|
API](Subscriptions.md#streaming-subscriptions). The
|
|
`AlarmStreamExtensions.SubscribeAlarmsAsync` extension returns
|
|
strongly-typed records:
|
|
|
|
```csharp
|
|
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`:
|
|
|
|
```csharp
|
|
// 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` whose base-type chain ends at `BaseEventType`,
|
|
the generator emits a `partial record {Type}Record` deriving from the
|
|
record of its parent type, exposing one init-only property per
|
|
declared field. The standard NodeSet produces the following hierarchy
|
|
(abridged — only the most commonly observed types are shown):
|
|
|
|
```
|
|
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 any of these types **automatically**
|
|
get their own `*TypeRecord` deriving from the closest standard
|
|
ancestor — no checked-in code, no manual class definitions. Add a
|
|
hand-written `partial record VibrationAlarmTypeRecord` in 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:
|
|
|
|
```csharp
|
|
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 of the OPC UA specification defines
|
|
the "ConditionId" as the NodeId of the condition object that fired
|
|
the event, which is reported through the `SourceNode` event field.
|
|
|
|
### Source-generated decoders + `EventRecordDecoderRegistry`
|
|
|
|
Every record emitted by the `EventRecordGenerator` also exposes a
|
|
nested `static class Decoder` with a positional `StandardFields`
|
|
table and a `Decode(IReadOnlyList<Variant>)` method that populates
|
|
own + inherited init-only properties. A per-file
|
|
`Register{ModelPrefix}Decoders(this EventRecordDecoderRegistry)`
|
|
extension registers every emitted decoder with a caller-supplied
|
|
registry.
|
|
|
|
The process-wide `EventRecordDecoderRegistry.Default` ships with the
|
|
standard UA model pre-registered. It routes by the event's
|
|
`EventType` field and walks the OPC UA event-type hierarchy through
|
|
an optional `SuperTypeResolver` when the exact type is not
|
|
registered. Vendor models register their generated extensions via
|
|
`Register{Prefix}Decoders` or
|
|
`CreateChildScope().Register{Prefix}Decoders()` for test isolation.
|
|
|
|
```csharp
|
|
// 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 produces an `EventFilter` whose where
|
|
clause restricts events to `OfType({recordTypeId})` and whose
|
|
select clauses come from the supplied registry's composed
|
|
`StandardFields` (defaults to
|
|
`EventRecordDecoderRegistry.Default`). The returned filter pairs
|
|
cleanly with `EventRecordDecoderRegistry.Decode` — the registry
|
|
remaps the composed positions to each decoder's own layout
|
|
before invoking it, so vendor models extend transparently.
|
|
|
|
```csharp
|
|
// 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);
|
|
```
|
|
|
|
The same registry should be passed to `Subscribe*Async`
|
|
(through the `registry:` parameter) so the streaming side decodes
|
|
through 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:
|
|
|
|
```csharp
|
|
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 `OkResponse` / `CancelResponse` / `DefaultResponse` properties on
|
|
the *server-side* `DialogConditionType` (Part 9 §5.6.2) carry their
|
|
canonical indices for clients to read separately via Read service if
|
|
the application needs them; they are not surfaced in the standard
|
|
`DialogConditionTypeRecord` (which only carries the dialog prompt +
|
|
active state).
|
|
|
|
### `ConditionRefresh`
|
|
|
|
Both Part 9 refresh methods are available:
|
|
|
|
```csharp
|
|
// 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 classic `ISubscription.ConditionRefreshAsync` on the streaming
|
|
subscription is also available — they are equivalent when the
|
|
`AlarmClient` and the `IStreamingSubscription` are bound to the same
|
|
session.
|
|
|
|
## Reference
|
|
|
|
- [OPC UA Part 9 — Alarms and Conditions](https://reference.opcfoundation.org/specs/OPC-10000-9/full)
|
|
- [IEC 62682](https://webstore.iec.ch/publication/61256) — Management of alarm systems for the process industries
|
|
- [ISA 18.2](https://www.isa.org/products/ansi-isa-18-2-2016-management-of-alarm-systems-) — Management of Alarm Systems for the Process Industries
|
|
- [Subscriptions and Monitored Items Service Set](Subscriptions.md) — `IStreamingSubscription` and the V2 subscription engine
|
|
- [State Machines](StateMachines.md) — generic Part 16 state-machine API used by
|
|
`AlarmClient.GetShelvingStateAsync` / `ObserveShelvingTransitionsAsync`
|
|
- [Model Change Tracking](ModelChangeTracking.md) — 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/ConsoleReferenceClient/AlarmClientSample.cs`
|
|
- Conformance tests: `tests/Opc.Ua.History.Tests/AlarmsAndConditions*.cs` |