Auto: twincat-5.1 — IAlarmSource via TC3 EventLogger (gated, scaffold)
Closes #316
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// PR 5.1 / #316 — stream TC3 EventLogger alarms to the terminal until Ctrl+C.
|
||||
/// Mirrors the OPC UA Client CLI <c>alarms</c> verb shape: subscribe + print every
|
||||
/// incoming <see cref="AlarmEventArgs"/> with timestamp, source, severity, and
|
||||
/// message text. Requires <c>EnableAlarms=true</c> on the driver — set via the
|
||||
/// base options builder when the verb is selected.
|
||||
/// </summary>
|
||||
[Command("alarms", Description =
|
||||
"Subscribe to TC3 EventLogger alarms via the driver's IAlarmSource bridge and " +
|
||||
"stream events to stdout until Ctrl+C.")]
|
||||
public sealed class AlarmsCommand : TwinCATCommandBase
|
||||
{
|
||||
[CommandOption("source", Description =
|
||||
"Optional alarm source filter (matched case-insensitively against the event's " +
|
||||
"Source field). Repeat the flag to match multiple sources; omit to subscribe to " +
|
||||
"every event the EventLogger surfaces.")]
|
||||
public IReadOnlyList<string> Sources { get; init; } = Array.Empty<string>();
|
||||
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
// Empty Tags + EnableAlarms=true builds a driver that opens only the alarm path.
|
||||
// TwinCATDriverOptions is a regular class with init-only properties — rebuild
|
||||
// the instance instead of using a record-style `with` clone.
|
||||
var baseOptions = BuildOptions([]);
|
||||
var options = new TwinCATDriverOptions
|
||||
{
|
||||
Devices = baseOptions.Devices,
|
||||
Tags = baseOptions.Tags,
|
||||
Probe = baseOptions.Probe,
|
||||
Timeout = baseOptions.Timeout,
|
||||
UseNativeNotifications = baseOptions.UseNativeNotifications,
|
||||
EnableControllerBrowse = baseOptions.EnableControllerBrowse,
|
||||
MaxArrayExpansion = baseOptions.MaxArrayExpansion,
|
||||
EnableAlarms = true,
|
||||
};
|
||||
|
||||
await using var driver = new TwinCATDriver(options, DriverInstanceId);
|
||||
IAlarmSubscriptionHandle? handle = null;
|
||||
try
|
||||
{
|
||||
await driver.InitializeAsync("{}", ct);
|
||||
|
||||
driver.OnAlarmEvent += (_, e) =>
|
||||
{
|
||||
var line =
|
||||
$"[{e.SourceTimestampUtc:HH:mm:ss.fff}] " +
|
||||
$"{e.SourceNodeId} " +
|
||||
$"sev={e.Severity} " +
|
||||
$"type={e.AlarmType} " +
|
||||
$"cond={e.ConditionId} " +
|
||||
$"\"{e.Message}\"";
|
||||
console.Output.WriteLine(line);
|
||||
};
|
||||
|
||||
handle = await driver.SubscribeAlarmsAsync(Sources, ct);
|
||||
|
||||
var filterDesc = Sources.Count == 0
|
||||
? "all sources"
|
||||
: $"sources [{string.Join(", ", Sources)}]";
|
||||
await console.Output.WriteLineAsync(
|
||||
$"Subscribed to TC3 EventLogger alarms on {AmsNetId}:{AmsPort} ({filterDesc}). Ctrl+C to stop.");
|
||||
await console.Output.WriteLineAsync(
|
||||
"Note: Beckhoff doesn't ship a managed TcEventLogger wrapper; the driver " +
|
||||
"uses a best-effort decode of AMS port 110 notifications. Some fields may " +
|
||||
"surface as 'Unknown' until a binary-protocol decoder lands.");
|
||||
try
|
||||
{
|
||||
await Task.Delay(System.Threading.Timeout.InfiniteTimeSpan, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected on Ctrl+C.
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (handle is not null)
|
||||
{
|
||||
try { await driver.UnsubscribeAlarmsAsync(handle, CancellationToken.None); }
|
||||
catch { /* teardown best-effort */ }
|
||||
}
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
224
src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATAlarmSource.cs
Normal file
224
src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATAlarmSource.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
using System.Collections.Concurrent;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
/// <summary>
|
||||
/// PR 5.1 / #316 — single TC3 EventLogger event payload exposed by
|
||||
/// <see cref="ITwinCATAlarmGate"/> + projected onto
|
||||
/// <see cref="AlarmEventArgs"/> for the driver's <see cref="IAlarmSource"/> surface.
|
||||
/// Carries the four fields the EventLogger surfaces on the wire (event class GUID /
|
||||
/// source name / severity / message text) plus the originating timestamp + an
|
||||
/// <c>Acked</c> flag so a re-fired event after operator acknowledgement is
|
||||
/// distinguishable from a fresh raise.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Beckhoff doesn't ship a managed <c>TcEventLogger</c> wrapper in the regular
|
||||
/// <c>Beckhoff.TwinCAT.Ads</c> v6 NuGet (the C++ TcCOM headers exist, the .NET ones
|
||||
/// don't — see <c>docs/v3/twincat-eventlogger-spike.md</c>). The production gate
|
||||
/// therefore opens a second <see cref="ITwinCATClient"/> against AMS port 110
|
||||
/// (<c>AMSPORT_EVENTLOG</c>) + adds a device notification on
|
||||
/// <c>ADSIGRP_TCEVENTLOG_ALARMS</c>; the binary-protocol decode is best-effort and
|
||||
/// unrecognised fields surface as <c>"Unknown"</c>.
|
||||
/// </remarks>
|
||||
public sealed record TwinCATAlarmEvent(
|
||||
string EventClass,
|
||||
string Source,
|
||||
ushort Severity,
|
||||
string Message,
|
||||
DateTimeOffset OccurrenceUtc,
|
||||
bool Acked);
|
||||
|
||||
/// <summary>
|
||||
/// PR 5.1 / #316 — driver-internal seam for the TC3 EventLogger wire path. Production
|
||||
/// opens a second <see cref="ITwinCATClient"/> against AMS port 110 + adds a device
|
||||
/// notification on the alarm-list index group; the binary-protocol decoder lands on
|
||||
/// a follow-up PR (see <c>docs/v3/twincat-eventlogger-spike.md</c>). Tests substitute
|
||||
/// a fake gate to drive synthetic events.
|
||||
/// </summary>
|
||||
public interface ITwinCATAlarmGate : IDisposable
|
||||
{
|
||||
/// <summary>Connect / register the device-notification subscription.</summary>
|
||||
Task StartAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Fired by the gate for every alarm transition the EventLogger surfaces (raise /
|
||||
/// clear / acknowledge). The driver's <see cref="TwinCATAlarmSource"/> projects this
|
||||
/// onto <see cref="AlarmEventArgs"/> for every active subscription.
|
||||
/// </summary>
|
||||
event EventHandler<TwinCATAlarmEvent>? OnAlarmEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Issue an acknowledge against the EventLogger for the supplied source / condition.
|
||||
/// Best-effort — the wire format is undocumented in managed code; the production
|
||||
/// gate writes the acknowledge index-group when reachable + returns silently otherwise.
|
||||
/// </summary>
|
||||
Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Snapshot of currently-active alarms — empty when the gate has no events buffered.</summary>
|
||||
IReadOnlyList<TwinCATAlarmEvent> ActiveAlarms { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR 5.1 / #316 — default no-op alarm gate. Keeps the construction path simple for
|
||||
/// deployments without TcEventLogger configured + for unit tests that exercise the
|
||||
/// <c>EnableAlarms=false</c> short-circuit. <see cref="OnAlarmEvent"/> never fires;
|
||||
/// <see cref="AcknowledgeAsync"/> is a no-op.
|
||||
/// </summary>
|
||||
internal sealed class NullTwinCATAlarmGate : ITwinCATAlarmGate
|
||||
{
|
||||
public IReadOnlyList<TwinCATAlarmEvent> ActiveAlarms => Array.Empty<TwinCATAlarmEvent>();
|
||||
|
||||
public event EventHandler<TwinCATAlarmEvent>? OnAlarmEvent;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Touch the event so the unused-warning analyzer keeps quiet without suppressing
|
||||
// the diagnostic outright. The handler list stays empty in production.
|
||||
_ = OnAlarmEvent;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements,
|
||||
CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR 5.1 / #316 — projects <see cref="ITwinCATAlarmGate"/> events onto the driver's
|
||||
/// <see cref="IAlarmSource"/> surface. Subscriptions filter by source-node id (each
|
||||
/// id is matched against <see cref="TwinCATAlarmEvent.Source"/>); an empty filter list
|
||||
/// subscribes to every event.
|
||||
/// </summary>
|
||||
internal sealed class TwinCATAlarmSource : IAsyncDisposable
|
||||
{
|
||||
private readonly TwinCATDriver _driver;
|
||||
private readonly ITwinCATAlarmGate _gate;
|
||||
private readonly ConcurrentDictionary<long, Subscription> _subs = new();
|
||||
private long _nextId;
|
||||
private bool _started;
|
||||
private readonly Lock _startLock = new();
|
||||
|
||||
public TwinCATAlarmSource(TwinCATDriver driver, ITwinCATAlarmGate gate)
|
||||
{
|
||||
_driver = driver;
|
||||
_gate = gate;
|
||||
_gate.OnAlarmEvent += OnGateAlarm;
|
||||
}
|
||||
|
||||
public async Task<IAlarmSubscriptionHandle> SubscribeAsync(
|
||||
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
|
||||
{
|
||||
var id = Interlocked.Increment(ref _nextId);
|
||||
var handle = new TwinCATAlarmSubscriptionHandle(id);
|
||||
_subs[id] = new Subscription(handle, [..sourceNodeIds]);
|
||||
|
||||
// First subscription wins the start race; subsequent subscribes find the gate
|
||||
// already connected. Single-shot start keeps the AMS-port-110 session count to
|
||||
// exactly one per driver instance.
|
||||
var shouldStart = false;
|
||||
lock (_startLock)
|
||||
{
|
||||
if (!_started)
|
||||
{
|
||||
_started = true;
|
||||
shouldStart = true;
|
||||
}
|
||||
}
|
||||
if (shouldStart)
|
||||
await _gate.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
return handle;
|
||||
}
|
||||
|
||||
public Task UnsubscribeAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
|
||||
{
|
||||
if (handle is TwinCATAlarmSubscriptionHandle h)
|
||||
_subs.TryRemove(h.Id, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken)
|
||||
=> _gate.AcknowledgeAsync(acknowledgements, cancellationToken);
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
_gate.OnAlarmEvent -= OnGateAlarm;
|
||||
_subs.Clear();
|
||||
try { _gate.Dispose(); } catch { }
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translate a raw <see cref="TwinCATAlarmEvent"/> from the gate into one
|
||||
/// <see cref="AlarmEventArgs"/> per matching subscription. Source-node-id filters
|
||||
/// match on case-insensitive equality against <see cref="TwinCATAlarmEvent.Source"/>;
|
||||
/// an empty subscription filter list passes every event through.
|
||||
/// </summary>
|
||||
private void OnGateAlarm(object? sender, TwinCATAlarmEvent evt)
|
||||
{
|
||||
var conditionId = $"{evt.Source}#{evt.EventClass}";
|
||||
var sourceTimestamp = evt.OccurrenceUtc.UtcDateTime;
|
||||
foreach (var sub in _subs.Values)
|
||||
{
|
||||
if (!sub.Matches(evt.Source)) continue;
|
||||
var args = new AlarmEventArgs(
|
||||
SubscriptionHandle: sub.Handle,
|
||||
SourceNodeId: evt.Source,
|
||||
ConditionId: conditionId,
|
||||
AlarmType: evt.EventClass,
|
||||
Message: evt.Message,
|
||||
Severity: MapSeverity(evt.Severity),
|
||||
SourceTimestampUtc: sourceTimestamp);
|
||||
_driver.InvokeAlarmEvent(args);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map TC3 EventLogger 0–255 severity values onto the driver-agnostic
|
||||
/// <see cref="AlarmSeverity"/> bucket. Buckets follow the same low-quartile cuts the
|
||||
/// OPC UA AC mapping in <c>docs/drivers/TwinCAT.md</c> documents.
|
||||
/// </summary>
|
||||
internal static AlarmSeverity MapSeverity(ushort raw) => raw switch
|
||||
{
|
||||
<= 64 => AlarmSeverity.Low,
|
||||
<= 128 => AlarmSeverity.Medium,
|
||||
<= 192 => AlarmSeverity.High,
|
||||
_ => AlarmSeverity.Critical,
|
||||
};
|
||||
|
||||
private sealed class Subscription
|
||||
{
|
||||
public Subscription(TwinCATAlarmSubscriptionHandle handle, IReadOnlyList<string> sourceFilters)
|
||||
{
|
||||
Handle = handle;
|
||||
SourceFilters = sourceFilters;
|
||||
}
|
||||
|
||||
public TwinCATAlarmSubscriptionHandle Handle { get; }
|
||||
public IReadOnlyList<string> SourceFilters { get; }
|
||||
|
||||
public bool Matches(string source)
|
||||
{
|
||||
if (SourceFilters.Count == 0) return true; // wildcard
|
||||
for (var i = 0; i < SourceFilters.Count; i++)
|
||||
{
|
||||
if (string.Equals(SourceFilters[i], source, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR 5.1 / #316 — handle returned by <see cref="TwinCATAlarmSource.SubscribeAsync"/>.
|
||||
/// </summary>
|
||||
public sealed record TwinCATAlarmSubscriptionHandle(long Id) : IAlarmSubscriptionHandle
|
||||
{
|
||||
public string DiagnosticId => $"twincat-alarm-sub-{Id}";
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
/// resolver land in PRs 2 and 3.
|
||||
/// </summary>
|
||||
public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable,
|
||||
IHostConnectivityProbe, IPerCallHostResolver, IDisposable, IAsyncDisposable
|
||||
IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource, IDisposable, IAsyncDisposable
|
||||
{
|
||||
private readonly TwinCATDriverOptions _options;
|
||||
private readonly string _driverInstanceId;
|
||||
@@ -17,13 +17,25 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
private readonly PollGroupEngine _poll;
|
||||
private readonly Dictionary<string, DeviceState> _devices = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, TwinCATTagDefinition> _tagsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TwinCATAlarmSource? _alarmSource;
|
||||
private DriverHealth _health = new(DriverState.Unknown, null, null);
|
||||
|
||||
public event EventHandler<DataChangeEventArgs>? OnDataChange;
|
||||
public event EventHandler<HostStatusChangedEventArgs>? OnHostStatusChanged;
|
||||
public event EventHandler<AlarmEventArgs>? OnAlarmEvent;
|
||||
|
||||
/// <summary>
|
||||
/// PR 5.1 / #316 — internal seam for <see cref="TwinCATAlarmSource"/> to raise
|
||||
/// <see cref="OnAlarmEvent"/> against the driver's public surface. Mirrors the
|
||||
/// <see cref="OnDataChange"/> raise pattern so capability invokers see one event
|
||||
/// source per <c>IAlarmSource</c> driver instance regardless of how many internal
|
||||
/// subscriptions are open.
|
||||
/// </summary>
|
||||
internal void InvokeAlarmEvent(AlarmEventArgs args) => OnAlarmEvent?.Invoke(this, args);
|
||||
|
||||
public TwinCATDriver(TwinCATDriverOptions options, string driverInstanceId,
|
||||
ITwinCATClientFactory? clientFactory = null)
|
||||
ITwinCATClientFactory? clientFactory = null,
|
||||
ITwinCATAlarmGate? alarmGate = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_options = options;
|
||||
@@ -33,6 +45,16 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
reader: ReadAsync,
|
||||
onChange: (handle, tagRef, snapshot) =>
|
||||
OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot)));
|
||||
|
||||
// PR 5.1 / #316 — only stand up the alarm source when the option is on. With the
|
||||
// option off, IAlarmSource.SubscribeAlarmsAsync returns a no-op handle so capability
|
||||
// negotiation still works without paying for the second AMS session against port
|
||||
// 110 / TcEventLogger that the production gate would open.
|
||||
if (_options.EnableAlarms)
|
||||
{
|
||||
var gate = alarmGate ?? new NullTwinCATAlarmGate();
|
||||
_alarmSource = new TwinCATAlarmSource(this, gate);
|
||||
}
|
||||
}
|
||||
|
||||
public string DriverInstanceId => _driverInstanceId;
|
||||
@@ -85,6 +107,16 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
foreach (var r in sub.Registrations) { try { r.Dispose(); } catch { } }
|
||||
_nativeSubs.Clear();
|
||||
|
||||
// PR 5.1 / #316 — alarm source teardown. Disposes the gate (closes the second
|
||||
// AMS-port-110 client + drops the device-notification handle in the production
|
||||
// path) + clears the subscription bookkeeping. Best-effort — a flaky teardown
|
||||
// should not block shutdown of the wire-data path.
|
||||
if (_alarmSource is not null)
|
||||
{
|
||||
try { await _alarmSource.DisposeAsync().ConfigureAwait(false); }
|
||||
catch { /* swallow — see comment above */ }
|
||||
}
|
||||
|
||||
await _poll.DisposeAsync().ConfigureAwait(false);
|
||||
foreach (var state in _devices.Values)
|
||||
{
|
||||
@@ -808,6 +840,43 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
new HostStatusChangedEventArgs(state.Options.HostAddress, old, newState));
|
||||
}
|
||||
|
||||
// ---- IAlarmSource (TC3 EventLogger bridge, #316) ----
|
||||
|
||||
/// <summary>
|
||||
/// PR 5.1 / #316 — subscribe to TC3 EventLogger alarms scoped to <paramref name="sourceNodeIds"/>.
|
||||
/// Each id matches against <see cref="TwinCATAlarmEvent.Source"/>; an empty list passes every
|
||||
/// event through the gate. Feature-gated — when <see cref="TwinCATDriverOptions.EnableAlarms"/>
|
||||
/// is <c>false</c> (the default), returns a sentinel handle without opening the AMS-port-110
|
||||
/// session. Capability negotiation succeeds either way + <see cref="OnAlarmEvent"/> simply
|
||||
/// never fires while the gate is disabled.
|
||||
/// </summary>
|
||||
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
||||
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_alarmSource is null)
|
||||
{
|
||||
// Disabled-gate sentinel — Id 0 is reserved for the no-op shape so a follow-up
|
||||
// unsubscribe doesn't accidentally remove a real subscription.
|
||||
var disabled = new TwinCATAlarmSubscriptionHandle(0);
|
||||
return Task.FromResult<IAlarmSubscriptionHandle>(disabled);
|
||||
}
|
||||
return _alarmSource.SubscribeAsync(sourceNodeIds, cancellationToken);
|
||||
}
|
||||
|
||||
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) =>
|
||||
_alarmSource is null
|
||||
? Task.CompletedTask
|
||||
: _alarmSource.UnsubscribeAsync(handle, cancellationToken);
|
||||
|
||||
public Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken) =>
|
||||
_alarmSource is null
|
||||
? Task.CompletedTask
|
||||
: _alarmSource.AcknowledgeAsync(acknowledgements, cancellationToken);
|
||||
|
||||
/// <summary>Test-only — <c>true</c> when an alarm source has been instantiated.</summary>
|
||||
internal bool HasAlarmSource => _alarmSource is not null;
|
||||
|
||||
// ---- IPerCallHostResolver ----
|
||||
|
||||
public string ResolveHost(string fullReference)
|
||||
|
||||
@@ -44,6 +44,23 @@ public sealed class TwinCATDriverOptions
|
||||
/// declared in <see cref="Tags"/> (those bypass the walker entirely).
|
||||
/// </summary>
|
||||
public int MaxArrayExpansion { get; init; } = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// PR 5.1 / #316 — opt-in TC3 EventLogger bridge. When <c>true</c>, the driver
|
||||
/// surfaces TwinCAT alarm events via <see cref="Core.Abstractions.IAlarmSource"/> by
|
||||
/// opening a second <c>AdsClient</c> against AMS port 110 (<c>AMSPORT_EVENTLOG</c>)
|
||||
/// and adding a device notification on <c>ADSIGRP_TCEVENTLOG_ALARMS</c>. Default
|
||||
/// <c>false</c> because (a) Beckhoff doesn't ship a managed <c>TcEventLogger</c>
|
||||
/// wrapper in the regular <c>Beckhoff.TwinCAT.Ads</c> v6 NuGet, so the binary-protocol
|
||||
/// decode is best-effort and fields may surface as <c>"Unknown"</c>; (b) deployments
|
||||
/// without TcEventLogger configured shouldn't pay the cost of a second AMS session
|
||||
/// + notification handle; (c) leaves the spike output (
|
||||
/// <c>docs/v3/twincat-eventlogger-spike.md</c>) as the source of truth for the wire
|
||||
/// decode while the implementation lands incrementally. When
|
||||
/// <see cref="EnableAlarms"/> is <c>false</c>, <see cref="Core.Abstractions.IAlarmSource"/>
|
||||
/// methods short-circuit to a no-op subscription so capability negotiation still works.
|
||||
/// </summary>
|
||||
public bool EnableAlarms { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user