docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
This commit is contained in:
Joseph Doherty
2026-05-28 08:10:17 -04:00
parent f9fc7dd2e1
commit 64e3fbe035
756 changed files with 9876 additions and 96 deletions
@@ -23,11 +23,20 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
private readonly IAlarmHistorianWriteBackend _backend;
/// <summary>
/// Initializes a new instance of the AahClientManagedAlarmEventWriter class.
/// </summary>
/// <param name="backend">The alarm historian write backend to delegate to.</param>
public AahClientManagedAlarmEventWriter(IAlarmHistorianWriteBackend backend)
{
_backend = backend ?? throw new ArgumentNullException(nameof(backend));
}
/// <summary>
/// Writes an array of alarm historian events asynchronously.
/// </summary>
/// <param name="events">The alarm events to write.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task<bool[]> WriteAsync(AlarmHistorianEventDto[] events, CancellationToken cancellationToken)
{
if (events is null || events.Length == 0)
@@ -79,6 +88,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// trinary <see cref="AlarmHistorianWriteOutcome"/>. Exposed for the production
/// <see cref="SdkAlarmHistorianWriteBackend"/> to share the mapping with tests.
/// </summary>
/// <param name="hresult">The HRESULT code from the SDK call.</param>
/// <param name="isCommunicationError">Indicates whether the error is a communication-class error.</param>
/// <param name="isMalformedInput">Indicates whether the input was malformed.</param>
public static AlarmHistorianWriteOutcome MapOutcome(int hresult, bool isCommunicationError, bool isMalformedInput)
{
// Order matters: malformed input is permanent regardless of HRESULT pattern;
@@ -16,9 +16,14 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
private readonly object _lock = new object();
private readonly List<NodeEntry> _nodes;
/// <summary>Initializes the picker with default system clock.</summary>
/// <param name="config">Historian configuration.</param>
public HistorianClusterEndpointPicker(HistorianConfiguration config)
: this(config, () => DateTime.UtcNow) { }
/// <summary>Initializes the picker with custom clock function.</summary>
/// <param name="config">Historian configuration.</param>
/// <param name="clock">Clock function for testing.</param>
internal HistorianClusterEndpointPicker(HistorianConfiguration config, Func<DateTime> clock)
{
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
@@ -36,11 +41,13 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
.ToList();
}
/// <summary>Gets the total count of configured nodes.</summary>
public int NodeCount
{
get { lock (_lock) return _nodes.Count; }
}
/// <summary>Gets the list of currently healthy nodes.</summary>
public IReadOnlyList<string> GetHealthyNodes()
{
lock (_lock)
@@ -50,6 +57,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
}
}
/// <summary>Gets the count of currently healthy nodes.</summary>
public int HealthyNodeCount
{
get
@@ -62,6 +70,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
}
}
/// <summary>Marks a node as failed and starts its cooldown.</summary>
/// <param name="node">Node name.</param>
/// <param name="error">Optional error message.</param>
public void MarkFailed(string node, string? error)
{
lock (_lock)
@@ -77,6 +88,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
}
}
/// <summary>Marks a node as healthy and clears its cooldown.</summary>
/// <param name="node">Node name.</param>
public void MarkHealthy(string node)
{
lock (_lock)
@@ -87,6 +100,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
}
}
/// <summary>Returns a snapshot of all node states.</summary>
public List<HistorianClusterNodeState> SnapshotNodeStates()
{
lock (_lock)
@@ -119,10 +133,15 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
private sealed class NodeEntry
{
/// <summary>Gets or sets the node name.</summary>
public string Name { get; set; } = "";
/// <summary>Gets or sets when cooldown expires.</summary>
public DateTime? CooldownUntil { get; set; }
/// <summary>Gets or sets the failure count.</summary>
public int FailureCount { get; set; }
/// <summary>Gets or sets the last error message.</summary>
public string? LastError { get; set; }
/// <summary>Gets or sets the last failure time.</summary>
public DateTime? LastFailureTime { get; set; }
}
}
@@ -8,11 +8,22 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// </summary>
public sealed class HistorianClusterNodeState
{
/// <summary>Gets or sets the node name.</summary>
public string Name { get; set; } = "";
/// <summary>Gets or sets a value indicating whether the node is healthy.</summary>
public bool IsHealthy { get; set; }
/// <summary>Gets or sets the time until the node exits cooldown mode.</summary>
public DateTime? CooldownUntil { get; set; }
/// <summary>Gets or sets the count of recent failures.</summary>
public int FailureCount { get; set; }
/// <summary>Gets or sets the last error message.</summary>
public string? LastError { get; set; }
/// <summary>Gets or sets the time of the last failure.</summary>
public DateTime? LastFailureTime { get; set; }
}
}
@@ -12,6 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// </summary>
public sealed class HistorianConfiguration
{
/// <summary>Gets or sets a value indicating whether Historian integration is enabled.</summary>
public bool Enabled { get; set; } = false;
/// <summary>Single-node fallback when <see cref="ServerNames"/> is empty.</summary>
@@ -24,12 +25,19 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// </summary>
public List<string> ServerNames { get; set; } = new();
/// <summary>Gets or sets the failure cooldown period in seconds.</summary>
public int FailureCooldownSeconds { get; set; } = 60;
/// <summary>Gets or sets a value indicating whether to use integrated security.</summary>
public bool IntegratedSecurity { get; set; } = true;
/// <summary>Gets or sets the user name for authentication.</summary>
public string? UserName { get; set; }
/// <summary>Gets or sets the password for authentication.</summary>
public string? Password { get; set; }
/// <summary>Gets or sets the Historian server port.</summary>
public int Port { get; set; } = 32568;
/// <summary>Gets or sets the command timeout in seconds.</summary>
public int CommandTimeoutSeconds { get; set; } = 30;
/// <summary>Gets or sets the maximum number of values per read operation.</summary>
public int MaxValuesPerRead { get; set; } = 10000;
/// <summary>
@@ -40,9 +40,15 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
private readonly HistorianClusterEndpointPicker _picker;
/// <summary>Initializes a new instance of the <see cref="HistorianDataSource"/> class with the default connection factory.</summary>
/// <param name="config">The historian configuration.</param>
public HistorianDataSource(HistorianConfiguration config)
: this(config, new SdkHistorianConnectionFactory(), null) { }
/// <summary>Initializes a new instance of the <see cref="HistorianDataSource"/> class with the specified connection factory and endpoint picker.</summary>
/// <param name="config">The historian configuration.</param>
/// <param name="factory">The historian connection factory.</param>
/// <param name="picker">The optional cluster endpoint picker.</param>
internal HistorianDataSource(
HistorianConfiguration config,
IHistorianConnectionFactory factory,
@@ -76,6 +82,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// <em>connection</em> (rather than the query payload) is the problem and the
/// shared SDK connection should therefore be reset. Internal for unit testing.
/// </summary>
/// <param name="code">The historian access error code.</param>
internal static bool IsConnectionClassError(HistorianAccessError.ErrorValue code)
=> ConnectionErrorCodes.Contains(code);
@@ -88,6 +95,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// single pipe-server connection thread indefinitely. See
/// Driver.Historian.Wonderware-010.
/// </summary>
/// <param name="cfg">The historian configuration.</param>
/// <param name="ct">The cancellation token.</param>
internal static CancellationTokenSource BuildRequestCts(HistorianConfiguration cfg, CancellationToken ct)
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
@@ -151,6 +160,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
};
}
/// <summary>Gets a snapshot of the current health status.</summary>
public HistorianHealthSnapshot GetHealthSnapshot()
{
var nodeStates = _picker.SnapshotNodeStates();
@@ -309,7 +319,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// </summary>
internal sealed class QueryClassStartQueryException : InvalidOperationException
{
/// <summary>Gets the error code that caused the exception.</summary>
public HistorianAccessError.ErrorValue Code { get; }
/// <summary>Initializes a new instance of the <see cref="QueryClassStartQueryException"/> class.</summary>
/// <param name="message">The exception message.</param>
/// <param name="code">The historian access error code.</param>
public QueryClassStartQueryException(string message, HistorianAccessError.ErrorValue code)
: base(message)
{
@@ -382,6 +396,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
}
}
/// <summary>Reads raw historical samples for the specified tag.</summary>
/// <param name="tagName">The tag name.</param>
/// <param name="startTime">The start time for the query.</param>
/// <param name="endTime">The end time for the query.</param>
/// <param name="maxValues">The maximum number of values to return.</param>
/// <param name="ct">Cancellation token for the operation.</param>
public Task<List<HistorianSample>> ReadRawAsync(
string tagName, DateTime startTime, DateTime endTime, int maxValues,
CancellationToken ct = default)
@@ -465,6 +485,13 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
return Task.FromResult(results);
}
/// <summary>Reads aggregate historical samples for the specified tag.</summary>
/// <param name="tagName">The tag name.</param>
/// <param name="startTime">The start time for the query.</param>
/// <param name="endTime">The end time for the query.</param>
/// <param name="intervalMs">The interval in milliseconds.</param>
/// <param name="aggregateColumn">The aggregate column name.</param>
/// <param name="ct">Cancellation token for the operation.</param>
public Task<List<HistorianAggregateSample>> ReadAggregateAsync(
string tagName, DateTime startTime, DateTime endTime,
double intervalMs, string aggregateColumn,
@@ -545,6 +572,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
return Task.FromResult(results);
}
/// <summary>Reads historical samples at specific timestamps for the specified tag.</summary>
/// <param name="tagName">The tag name.</param>
/// <param name="timestamps">The timestamps to read.</param>
/// <param name="ct">Cancellation token for the operation.</param>
public Task<List<HistorianSample>> ReadAtTimeAsync(
string tagName, DateTime[] timestamps,
CancellationToken ct = default)
@@ -627,6 +658,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
return Task.FromResult(results);
}
/// <summary>Reads historical events within the specified time range.</summary>
/// <param name="sourceName">The optional event source name filter.</param>
/// <param name="startTime">The start time for the query.</param>
/// <param name="endTime">The end time for the query.</param>
/// <param name="maxEvents">The maximum number of events to return.</param>
/// <param name="ct">Cancellation token for the operation.</param>
public Task<List<HistorianEventDto>> ReadEventsAsync(
string? sourceName, DateTime startTime, DateTime endTime, int maxEvents,
CancellationToken ct = default)
@@ -726,6 +763,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// as a string; this is a known edge case of the SDK binding.
/// </para>
/// </summary>
/// <param name="result">The history query result.</param>
internal static object? SelectValue(HistoryQueryResult result)
=> SelectValueFromPair(result.Value, result.StringValue);
@@ -735,6 +773,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// <see cref="HistoryQueryResult"/> (whose internal property initialisers make
/// it impractical to fake). See Driver.Historian.Wonderware-012.
/// </summary>
/// <param name="value">The numeric value.</param>
/// <param name="stringValue">The string value.</param>
internal static object? SelectValueFromPair(double value, string? stringValue)
{
if (!string.IsNullOrEmpty(stringValue) && value == 0)
@@ -742,6 +782,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
return value;
}
/// <summary>Extracts the specified aggregate value from an analog summary query result.</summary>
/// <param name="result">The analog summary query result.</param>
/// <param name="column">The aggregate column name.</param>
internal static double? ExtractAggregateValue(AnalogSummaryQueryResult result, string column)
{
switch (column)
@@ -757,6 +800,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
}
}
/// <summary>Disposes the historian data source and releases its resources.</summary>
public void Dispose()
{
if (_disposed) return;
@@ -8,11 +8,22 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// </summary>
public sealed class HistorianEventDto
{
/// <summary>Gets or sets the unique identifier for the event.</summary>
public Guid Id { get; set; }
/// <summary>Gets or sets the source of the event.</summary>
public string? Source { get; set; }
/// <summary>Gets or sets the time when the event occurred.</summary>
public DateTime EventTime { get; set; }
/// <summary>Gets or sets the time when the event was received.</summary>
public DateTime ReceivedTime { get; set; }
/// <summary>Gets or sets the display text for the event.</summary>
public string? DisplayText { get; set; }
/// <summary>Gets or sets the severity level of the event.</summary>
public ushort Severity { get; set; }
}
}
@@ -9,19 +9,33 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// </summary>
public sealed class HistorianHealthSnapshot
{
/// <summary>Gets or sets the total number of queries executed.</summary>
public long TotalQueries { get; set; }
/// <summary>Gets or sets the total number of successful queries.</summary>
public long TotalSuccesses { get; set; }
/// <summary>Gets or sets the total number of failed queries.</summary>
public long TotalFailures { get; set; }
/// <summary>Gets or sets the number of consecutive failures.</summary>
public int ConsecutiveFailures { get; set; }
/// <summary>Gets or sets the time of the last successful query.</summary>
public DateTime? LastSuccessTime { get; set; }
/// <summary>Gets or sets the time of the last failed query.</summary>
public DateTime? LastFailureTime { get; set; }
/// <summary>Gets or sets the last error message, if any.</summary>
public string? LastError { get; set; }
/// <summary>Gets or sets a value indicating whether the process connection is open.</summary>
public bool ProcessConnectionOpen { get; set; }
/// <summary>Gets or sets a value indicating whether the event connection is open.</summary>
public bool EventConnectionOpen { get; set; }
/// <summary>Gets or sets the name of the active process node.</summary>
public string? ActiveProcessNode { get; set; }
/// <summary>Gets or sets the name of the active event node.</summary>
public string? ActiveEventNode { get; set; }
/// <summary>Gets or sets the total number of cluster nodes.</summary>
public int NodeCount { get; set; }
/// <summary>Gets or sets the number of healthy cluster nodes.</summary>
public int HealthyNodeCount { get; set; }
/// <summary>Gets or sets the list of cluster node states.</summary>
public List<HistorianClusterNodeState> Nodes { get; set; } = new();
}
}
@@ -15,6 +15,8 @@ public static class HistorianQualityMapper
/// family bits decide the category (Good &gt;= 192, Uncertain 64-191, Bad 0-63); the
/// low-nibble subcode selects the specific code.
/// </summary>
/// <param name="q">The OPC DA quality byte.</param>
/// <returns>The corresponding OPC UA status code.</returns>
public static uint Map(byte q) => q switch
{
// Good family (192+)
@@ -11,11 +11,13 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// </summary>
public sealed class HistorianSample
{
/// <summary>Gets or sets the historical data value.</summary>
public object? Value { get; set; }
/// <summary>Raw OPC DA quality byte from the historian SDK (low 8 bits of OpcQuality).</summary>
/// <summary>Gets or sets the raw OPC DA quality byte from the historian SDK (low 8 bits of OpcQuality).</summary>
public byte Quality { get; set; }
/// <summary>Gets or sets the UTC timestamp of the historical sample.</summary>
public DateTime TimestampUtc { get; set; }
}
@@ -25,7 +27,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// </summary>
public sealed class HistorianAggregateSample
{
/// <summary>Gets or sets the aggregate value, or null if unavailable.</summary>
public double? Value { get; set; }
/// <summary>Gets or sets the UTC timestamp of the aggregate sample.</summary>
public DateTime TimestampUtc { get; set; }
}
}
@@ -23,6 +23,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// input slot in the same order — must always return an array of the same
/// length as <paramref name="events"/>.
/// </summary>
/// <param name="events">The events to write to the historian.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
Task<AlarmHistorianWriteOutcome[]> WriteBatchAsync(
AlarmHistorianEventDto[] events,
CancellationToken cancellationToken);
@@ -16,6 +16,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// <c>false</c> because <c>HistorianAccess.AddStreamedValue</c> fails with
/// <c>WriteToReadOnlyFile</c> on a read-only session.
/// </summary>
/// <param name="config">The historian configuration.</param>
/// <param name="type">The type of connection to create.</param>
/// <param name="readOnly">Whether the connection should be read-only.</param>
/// <returns>An open HistorianAccess connection.</returns>
HistorianAccess CreateAndConnect(
HistorianConfiguration config, HistorianConnectionType type, bool readOnly = true);
}
@@ -23,6 +27,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// <summary>Production implementation — opens real Historian SDK connections.</summary>
internal sealed class SdkHistorianConnectionFactory : IHistorianConnectionFactory
{
/// <summary>Creates and connects a Historian SDK connection.</summary>
/// <param name="config">The historian configuration.</param>
/// <param name="type">The type of connection to create.</param>
/// <param name="readOnly">Whether the connection should be read-only.</param>
/// <returns>An open HistorianAccess connection.</returns>
public HistorianAccess CreateAndConnect(
HistorianConfiguration config, HistorianConnectionType type, bool readOnly = true)
{
@@ -66,6 +75,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// Builds the <see cref="HistorianConnectionArgs"/> for a connection. Pure (no SDK
/// side effects) so the read-only-vs-write argument shaping is unit-testable.
/// </summary>
/// <param name="config">The historian configuration.</param>
/// <param name="type">The type of connection to create.</param>
/// <param name="readOnly">Whether the connection should be read-only.</param>
/// <returns>The configured connection arguments.</returns>
internal static HistorianConnectionArgs BuildConnectionArgs(
HistorianConfiguration config, HistorianConnectionType type, bool readOnly)
{
@@ -14,23 +14,52 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// </summary>
public interface IHistorianDataSource : IDisposable
{
/// <summary>Reads raw historical samples asynchronously.</summary>
/// <param name="tagName">The tag name to read from.</param>
/// <param name="startTime">The start time of the time range.</param>
/// <param name="endTime">The end time of the time range.</param>
/// <param name="maxValues">The maximum number of values to return.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation that returns a list of historian samples.</returns>
Task<List<HistorianSample>> ReadRawAsync(
string tagName, DateTime startTime, DateTime endTime, int maxValues,
CancellationToken ct = default);
/// <summary>Reads aggregate historical samples asynchronously.</summary>
/// <param name="tagName">The tag name to read from.</param>
/// <param name="startTime">The start time of the time range.</param>
/// <param name="endTime">The end time of the time range.</param>
/// <param name="intervalMs">The interval in milliseconds for aggregation.</param>
/// <param name="aggregateColumn">The column to aggregate.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation that returns a list of aggregate samples.</returns>
Task<List<HistorianAggregateSample>> ReadAggregateAsync(
string tagName, DateTime startTime, DateTime endTime,
double intervalMs, string aggregateColumn,
CancellationToken ct = default);
/// <summary>Reads historical samples at specific times asynchronously.</summary>
/// <param name="tagName">The tag name to read from.</param>
/// <param name="timestamps">The array of timestamps at which to read values.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation that returns a list of historian samples.</returns>
Task<List<HistorianSample>> ReadAtTimeAsync(
string tagName, DateTime[] timestamps,
CancellationToken ct = default);
/// <summary>Reads historical events asynchronously.</summary>
/// <param name="sourceName">The source name to filter events, or null for all sources.</param>
/// <param name="startTime">The start time of the time range.</param>
/// <param name="endTime">The end time of the time range.</param>
/// <param name="maxEvents">The maximum number of events to return.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation that returns a list of historian events.</returns>
Task<List<HistorianEventDto>> ReadEventsAsync(
string? sourceName, DateTime startTime, DateTime endTime, int maxEvents,
CancellationToken ct = default);
/// <summary>Gets a health snapshot of the data source.</summary>
/// <returns>A HistorianHealthSnapshot containing the current health information.</returns>
HistorianHealthSnapshot GetHealthSnapshot();
}
}
@@ -84,9 +84,15 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
private string? _activeNode;
private bool _disposed;
/// <summary>Initializes a new instance using the default SDK connection factory.</summary>
/// <param name="config">The historian configuration.</param>
public SdkAlarmHistorianWriteBackend(HistorianConfiguration config)
: this(config, new SdkHistorianConnectionFactory(), null) { }
/// <summary>Initializes a new instance with injected dependencies (for testing).</summary>
/// <param name="config">The historian configuration.</param>
/// <param name="factory">The connection factory.</param>
/// <param name="picker">The cluster endpoint picker, or null to use a new instance.</param>
internal SdkAlarmHistorianWriteBackend(
HistorianConfiguration config,
IHistorianConnectionFactory factory,
@@ -97,6 +103,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
_picker = picker ?? new HistorianClusterEndpointPicker(config);
}
/// <summary>Writes a batch of alarm events to the historian, returning outcomes for each event.</summary>
/// <param name="events">The alarm events to write.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>An array of outcomes corresponding to each input event.</returns>
public Task<AlarmHistorianWriteOutcome[]> WriteBatchAsync(
AlarmHistorianEventDto[] events,
CancellationToken cancellationToken)
@@ -183,6 +193,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// event properties — operator-comment fidelity is the field the value-driven
/// fallback path cannot carry.
/// </summary>
/// <param name="dto">The alarm event data transfer object.</param>
/// <returns>The mapped HistorianEvent.</returns>
internal static HistorianEvent ToHistorianEvent(AlarmHistorianEventDto dto)
{
// The ArchestrA SDK marks these HistorianEvent members obsolete but still honours
@@ -238,6 +250,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
/// <see cref="AahClientManagedAlarmEventWriter.MapOutcome"/> mapping. Exposed for
/// unit tests — connection-class codes are handled separately by the batch loop.
/// </summary>
/// <param name="code">The error code to classify.</param>
/// <returns>The corresponding write outcome.</returns>
internal static AlarmHistorianWriteOutcome ClassifyOutcome(HistorianAccessError.ErrorValue code)
=> AahClientManagedAlarmEventWriter.MapOutcome(
(int)code,
@@ -365,6 +379,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend
RequestTimeoutSeconds = _config.RequestTimeoutSeconds,
};
/// <summary>Disposes the connection and releases resources.</summary>
public void Dispose()
{
if (_disposed) return;
@@ -22,6 +22,7 @@ public sealed class HistorianSampleDto
/// <summary>Raw OPC DA quality byte from the historian SDK (low 8 bits of OpcQuality).</summary>
[Key(1)] public byte Quality { get; set; }
/// <summary>Gets or sets the timestamp in UTC ticks.</summary>
[Key(2)] public long TimestampUtcTicks { get; set; }
}
@@ -29,7 +30,10 @@ public sealed class HistorianSampleDto
[MessagePackObject]
public sealed class HistorianAggregateSampleDto
{
/// <summary>Gets or sets the aggregate value.</summary>
[Key(0)] public double? Value { get; set; }
/// <summary>Gets or sets the timestamp in UTC ticks.</summary>
[Key(1)] public long TimestampUtcTicks { get; set; }
}
@@ -37,11 +41,22 @@ public sealed class HistorianAggregateSampleDto
[MessagePackObject]
public sealed class HistorianEventDto
{
/// <summary>Gets or sets the event identifier.</summary>
[Key(0)] public string EventId { get; set; } = string.Empty;
/// <summary>Gets or sets the event source name.</summary>
[Key(1)] public string? Source { get; set; }
/// <summary>Gets or sets the event time in UTC ticks.</summary>
[Key(2)] public long EventTimeUtcTicks { get; set; }
/// <summary>Gets or sets the received time in UTC ticks.</summary>
[Key(3)] public long ReceivedTimeUtcTicks { get; set; }
/// <summary>Gets or sets the display text.</summary>
[Key(4)] public string? DisplayText { get; set; }
/// <summary>Gets or sets the severity.</summary>
[Key(5)] public ushort Severity { get; set; }
}
@@ -49,13 +64,28 @@ public sealed class HistorianEventDto
[MessagePackObject]
public sealed class AlarmHistorianEventDto
{
/// <summary>Gets or sets the event identifier.</summary>
[Key(0)] public string EventId { get; set; } = string.Empty;
/// <summary>Gets or sets the source name.</summary>
[Key(1)] public string SourceName { get; set; } = string.Empty;
/// <summary>Gets or sets the condition identifier.</summary>
[Key(2)] public string? ConditionId { get; set; }
/// <summary>Gets or sets the alarm type.</summary>
[Key(3)] public string AlarmType { get; set; } = string.Empty;
/// <summary>Gets or sets the alarm message.</summary>
[Key(4)] public string? Message { get; set; }
/// <summary>Gets or sets the severity.</summary>
[Key(5)] public ushort Severity { get; set; }
/// <summary>Gets or sets the event time in UTC ticks.</summary>
[Key(6)] public long EventTimeUtcTicks { get; set; }
/// <summary>Gets or sets the acknowledgment comment.</summary>
[Key(7)] public string? AckComment { get; set; }
}
@@ -64,19 +94,35 @@ public sealed class AlarmHistorianEventDto
[MessagePackObject]
public sealed class ReadRawRequest
{
/// <summary>Gets or sets the tag name.</summary>
[Key(0)] public string TagName { get; set; } = string.Empty;
/// <summary>Gets or sets the start time in UTC ticks.</summary>
[Key(1)] public long StartUtcTicks { get; set; }
/// <summary>Gets or sets the end time in UTC ticks.</summary>
[Key(2)] public long EndUtcTicks { get; set; }
/// <summary>Gets or sets the maximum number of values to return.</summary>
[Key(3)] public int MaxValues { get; set; }
/// <summary>Gets or sets the correlation identifier.</summary>
[Key(4)] public string CorrelationId { get; set; } = string.Empty;
}
[MessagePackObject]
public sealed class ReadRawReply
{
/// <summary>Gets or sets the correlation identifier.</summary>
[Key(0)] public string CorrelationId { get; set; } = string.Empty;
/// <summary>Gets or sets a value indicating whether the request succeeded.</summary>
[Key(1)] public bool Success { get; set; }
/// <summary>Gets or sets the error message if the request failed.</summary>
[Key(2)] public string? Error { get; set; }
/// <summary>Gets or sets the historical samples.</summary>
[Key(3)] public HistorianSampleDto[] Samples { get; set; } = Array.Empty<HistorianSampleDto>();
}
@@ -85,9 +131,16 @@ public sealed class ReadRawReply
[MessagePackObject]
public sealed class ReadProcessedRequest
{
/// <summary>Gets or sets the tag name.</summary>
[Key(0)] public string TagName { get; set; } = string.Empty;
/// <summary>Gets or sets the start time in UTC ticks.</summary>
[Key(1)] public long StartUtcTicks { get; set; }
/// <summary>Gets or sets the end time in UTC ticks.</summary>
[Key(2)] public long EndUtcTicks { get; set; }
/// <summary>Gets or sets the interval in milliseconds.</summary>
[Key(3)] public double IntervalMs { get; set; }
/// <summary>
@@ -95,15 +148,24 @@ public sealed class ReadProcessedRequest
/// The .NET 10 client maps OPC UA aggregate enum → column.
/// </summary>
[Key(4)] public string AggregateColumn { get; set; } = string.Empty;
/// <summary>Gets or sets the correlation identifier.</summary>
[Key(5)] public string CorrelationId { get; set; } = string.Empty;
}
[MessagePackObject]
public sealed class ReadProcessedReply
{
/// <summary>Gets or sets the correlation identifier.</summary>
[Key(0)] public string CorrelationId { get; set; } = string.Empty;
/// <summary>Gets or sets a value indicating whether the request succeeded.</summary>
[Key(1)] public bool Success { get; set; }
/// <summary>Gets or sets the error message if the request failed.</summary>
[Key(2)] public string? Error { get; set; }
/// <summary>Gets or sets the aggregate sample buckets.</summary>
[Key(3)] public HistorianAggregateSampleDto[] Buckets { get; set; } = Array.Empty<HistorianAggregateSampleDto>();
}
@@ -112,17 +174,29 @@ public sealed class ReadProcessedReply
[MessagePackObject]
public sealed class ReadAtTimeRequest
{
/// <summary>Gets or sets the tag name.</summary>
[Key(0)] public string TagName { get; set; } = string.Empty;
/// <summary>Gets or sets the timestamps in UTC ticks.</summary>
[Key(1)] public long[] TimestampsUtcTicks { get; set; } = Array.Empty<long>();
/// <summary>Gets or sets the correlation identifier.</summary>
[Key(2)] public string CorrelationId { get; set; } = string.Empty;
}
[MessagePackObject]
public sealed class ReadAtTimeReply
{
/// <summary>Gets or sets the correlation identifier.</summary>
[Key(0)] public string CorrelationId { get; set; } = string.Empty;
/// <summary>Gets or sets a value indicating whether the request succeeded.</summary>
[Key(1)] public bool Success { get; set; }
/// <summary>Gets or sets the error message if the request failed.</summary>
[Key(2)] public string? Error { get; set; }
/// <summary>Gets or sets the historical samples.</summary>
[Key(3)] public HistorianSampleDto[] Samples { get; set; } = Array.Empty<HistorianSampleDto>();
}
@@ -131,19 +205,35 @@ public sealed class ReadAtTimeReply
[MessagePackObject]
public sealed class ReadEventsRequest
{
/// <summary>Gets or sets the source name.</summary>
[Key(0)] public string? SourceName { get; set; }
/// <summary>Gets or sets the start time in UTC ticks.</summary>
[Key(1)] public long StartUtcTicks { get; set; }
/// <summary>Gets or sets the end time in UTC ticks.</summary>
[Key(2)] public long EndUtcTicks { get; set; }
/// <summary>Gets or sets the maximum number of events to return.</summary>
[Key(3)] public int MaxEvents { get; set; }
/// <summary>Gets or sets the correlation identifier.</summary>
[Key(4)] public string CorrelationId { get; set; } = string.Empty;
}
[MessagePackObject]
public sealed class ReadEventsReply
{
/// <summary>Gets or sets the correlation identifier.</summary>
[Key(0)] public string CorrelationId { get; set; } = string.Empty;
/// <summary>Gets or sets a value indicating whether the request succeeded.</summary>
[Key(1)] public bool Success { get; set; }
/// <summary>Gets or sets the error message if the request failed.</summary>
[Key(2)] public string? Error { get; set; }
/// <summary>Gets or sets the historian events.</summary>
[Key(3)] public HistorianEventDto[] Events { get; set; } = Array.Empty<HistorianEventDto>();
}
@@ -152,15 +242,23 @@ public sealed class ReadEventsReply
[MessagePackObject]
public sealed class WriteAlarmEventsRequest
{
/// <summary>Gets or sets the alarm events to write.</summary>
[Key(0)] public AlarmHistorianEventDto[] Events { get; set; } = Array.Empty<AlarmHistorianEventDto>();
/// <summary>Gets or sets the correlation identifier.</summary>
[Key(1)] public string CorrelationId { get; set; } = string.Empty;
}
[MessagePackObject]
public sealed class WriteAlarmEventsReply
{
/// <summary>Gets or sets the correlation identifier.</summary>
[Key(0)] public string CorrelationId { get; set; } = string.Empty;
/// <summary>Gets or sets a value indicating whether the request succeeded.</summary>
[Key(1)] public bool Success { get; set; }
/// <summary>Gets or sets the error message if the request failed.</summary>
[Key(2)] public string? Error { get; set; }
/// <summary>Per-event success flag, parallel to <see cref="WriteAlarmEventsRequest.Events"/>.</summary>
@@ -17,12 +17,18 @@ public sealed class FrameReader : IDisposable
private readonly Stream _stream;
private readonly bool _leaveOpen;
/// <summary>Initializes a new instance of the <see cref="FrameReader"/> class.</summary>
/// <param name="stream">The stream to read frames from.</param>
/// <param name="leaveOpen">Whether to leave the stream open when disposing.</param>
public FrameReader(Stream stream, bool leaveOpen = false)
{
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
_leaveOpen = leaveOpen;
}
/// <summary>Reads the next frame asynchronously from the stream.</summary>
/// <param name="ct">Cancellation token for the operation.</param>
/// <returns>A tuple of message kind and body, or null if EOF is encountered cleanly.</returns>
public async Task<(MessageKind Kind, byte[] Body)?> ReadFrameAsync(CancellationToken ct)
{
var lengthPrefix = new byte[Framing.LengthPrefixSize];
@@ -43,6 +49,9 @@ public sealed class FrameReader : IDisposable
return ((MessageKind)(byte)kindByte, body);
}
/// <summary>Deserializes the message body to the specified type.</summary>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <param name="body">The serialized message body.</param>
public static T Deserialize<T>(byte[] body) => MessagePackSerializer.Deserialize<T>(body);
private async Task<bool> ReadExactAsync(byte[] buffer, CancellationToken ct)
@@ -61,6 +70,7 @@ public sealed class FrameReader : IDisposable
return true;
}
/// <summary>Disposes the frame reader and optionally closes the underlying stream.</summary>
public void Dispose()
{
if (!_leaveOpen) _stream.Dispose();
@@ -18,12 +18,20 @@ public sealed class FrameWriter : IDisposable
private readonly SemaphoreSlim _gate = new(1, 1);
private readonly bool _leaveOpen;
/// <summary>Initializes a new instance of the FrameWriter.</summary>
/// <param name="stream">The stream to write frames to.</param>
/// <param name="leaveOpen">Whether to leave the stream open when disposed.</param>
public FrameWriter(Stream stream, bool leaveOpen = false)
{
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
_leaveOpen = leaveOpen;
}
/// <summary>Writes a frame with the specified message kind and serialized message.</summary>
/// <typeparam name="T">The type of message being written.</typeparam>
/// <param name="kind">The message kind identifier.</param>
/// <param name="message">The message to serialize and write.</param>
/// <param name="ct">The cancellation token.</param>
public async Task WriteAsync<T>(MessageKind kind, T message, CancellationToken ct)
{
var body = MessagePackSerializer.Serialize(message, cancellationToken: ct);
@@ -49,6 +57,7 @@ public sealed class FrameWriter : IDisposable
finally { _gate.Release(); }
}
/// <summary>Disposes the frame writer and releases resources.</summary>
public void Dispose()
{
_gate.Dispose();
@@ -12,21 +12,30 @@ public sealed class Hello
public const int CurrentMajor = 1;
public const int CurrentMinor = 0;
/// <summary>Gets or sets the protocol major version.</summary>
[Key(0)] public int ProtocolMajor { get; set; } = CurrentMajor;
/// <summary>Gets or sets the protocol minor version.</summary>
[Key(1)] public int ProtocolMinor { get; set; } = CurrentMinor;
/// <summary>Gets or sets the peer name.</summary>
[Key(2)] public string PeerName { get; set; } = string.Empty;
/// <summary>Per-process shared secret — verified against the value the supervisor passed at spawn time.</summary>
[Key(3)] public string SharedSecret { get; set; } = string.Empty;
}
/// <summary>Response to a Hello handshake message.</summary>
[MessagePackObject]
public sealed class HelloAck
{
/// <summary>Gets or sets the protocol major version.</summary>
[Key(0)] public int ProtocolMajor { get; set; } = Hello.CurrentMajor;
/// <summary>Gets or sets the protocol minor version.</summary>
[Key(1)] public int ProtocolMinor { get; set; } = Hello.CurrentMinor;
/// <summary>Gets or sets a value indicating whether the handshake was accepted.</summary>
[Key(2)] public bool Accepted { get; set; }
/// <summary>Gets or sets the rejection reason if Accepted is false.</summary>
[Key(3)] public string? RejectReason { get; set; }
/// <summary>Gets or sets the host name of the server.</summary>
[Key(4)] public string HostName { get; set; } = string.Empty;
}
@@ -20,6 +20,10 @@ public sealed class HistorianFrameHandler : IFrameHandler
private readonly IAlarmEventWriter? _alarmWriter;
private readonly ILogger _logger;
/// <summary>Initializes a new instance of the HistorianFrameHandler class.</summary>
/// <param name="historian">The historian data source to query.</param>
/// <param name="logger">The logger instance.</param>
/// <param name="alarmWriter">Optional alarm event writer for writebacks.</param>
public HistorianFrameHandler(
IHistorianDataSource historian,
ILogger logger,
@@ -30,6 +34,11 @@ public sealed class HistorianFrameHandler : IFrameHandler
_alarmWriter = alarmWriter;
}
/// <summary>Handles an incoming frame by dispatching to the appropriate historian operation.</summary>
/// <param name="kind">The frame message kind.</param>
/// <param name="body">The frame body bytes.</param>
/// <param name="writer">The frame writer for sending responses.</param>
/// <param name="ct">Cancellation token.</param>
public Task HandleAsync(MessageKind kind, byte[] body, FrameWriter writer, CancellationToken ct)
=> kind switch
{
@@ -251,5 +260,7 @@ public interface IAlarmEventWriter
/// persisted vs. retry-please. The SQLite store-and-forward sink retries failed
/// slots on the next drain tick.
/// </summary>
/// <param name="events">Alarm events to write.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<bool[]> WriteAsync(AlarmHistorianEventDto[] events, CancellationToken cancellationToken);
}
@@ -13,6 +13,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Ipc;
/// </summary>
public static class PipeAcl
{
/// <summary>Creates a strict PipeSecurity for the historian sidecar pipe.</summary>
/// <param name="allowedSid">The security identifier that should have read-write access to the pipe.</param>
/// <returns>A configured PipeSecurity object with strict access control.</returns>
public static PipeSecurity Create(SecurityIdentifier allowedSid)
{
if (allowedSid is null) throw new ArgumentNullException(nameof(allowedSid));
@@ -29,11 +29,25 @@ public sealed class PipeServer : IDisposable
/// <see cref="VerifyCaller"/>; tests can substitute one that ignores the pipe ACL
/// to exercise the rejection paths.
/// </summary>
/// <param name="pipe">The named pipe server stream to verify.</param>
/// <param name="allowedSid">The allowed security identifier.</param>
/// <param name="reason">The rejection reason if verification fails.</param>
internal delegate bool CallerVerifier(NamedPipeServerStream pipe, SecurityIdentifier allowedSid, out string reason);
/// <summary>Initializes a new instance of the <see cref="PipeServer"/> class.</summary>
/// <param name="pipeName">The name of the named pipe.</param>
/// <param name="allowedSid">The security identifier allowed to connect.</param>
/// <param name="sharedSecret">The shared secret for client authentication.</param>
/// <param name="logger">The logger for diagnostic messages.</param>
public PipeServer(string pipeName, SecurityIdentifier allowedSid, string sharedSecret, ILogger logger)
: this(pipeName, allowedSid, sharedSecret, logger, DefaultVerifier) { }
/// <summary>Initializes a new instance of the <see cref="PipeServer"/> class with a custom verifier.</summary>
/// <param name="pipeName">The name of the named pipe.</param>
/// <param name="allowedSid">The security identifier allowed to connect.</param>
/// <param name="sharedSecret">The shared secret for client authentication.</param>
/// <param name="logger">The logger for diagnostic messages.</param>
/// <param name="verifier">The caller verification delegate.</param>
internal PipeServer(
string pipeName, SecurityIdentifier allowedSid, string sharedSecret, ILogger logger,
CallerVerifier verifier)
@@ -52,6 +66,8 @@ public sealed class PipeServer : IDisposable
/// Accepts one connection, performs Hello handshake, then dispatches frames to
/// <paramref name="handler"/> until EOF or cancel. Returns when the client disconnects.
/// </summary>
/// <param name="handler">The frame handler to process frames.</param>
/// <param name="ct">Cancellation token for the operation.</param>
public async Task RunOneConnectionAsync(IFrameHandler handler, CancellationToken ct)
{
using var linked = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, ct);
@@ -161,6 +177,8 @@ public sealed class PipeServer : IDisposable
/// If <see cref="MaxConsecutiveFailures"/> consecutive failures occur the method
/// throws so the supervisor can restart the sidecar.
/// </summary>
/// <param name="handler">The frame handler to process frames.</param>
/// <param name="ct">Cancellation token for the operation.</param>
public async Task RunAsync(IFrameHandler handler, CancellationToken ct)
{
var consecutiveFailures = 0;
@@ -215,6 +233,7 @@ public sealed class PipeServer : IDisposable
catch (Exception ex) { reason = ex.Message; return false; }
}
/// <summary>Disposes the pipe server and cancels any pending operations.</summary>
public void Dispose()
{
_cts.Cancel();
@@ -230,5 +249,10 @@ public sealed class PipeServer : IDisposable
/// </summary>
public interface IFrameHandler
{
/// <summary>Handles a frame from the pipe server.</summary>
/// <param name="kind">The type of message being handled.</param>
/// <param name="body">The serialized message body.</param>
/// <param name="writer">The frame writer to send responses.</param>
/// <param name="ct">Cancellation token for the operation.</param>
Task HandleAsync(MessageKind kind, byte[] body, FrameWriter writer, CancellationToken ct);
}
@@ -15,6 +15,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware;
/// </summary>
public static class Program
{
/// <summary>Entry point for the Wonderware Historian sidecar process.</summary>
/// <param name="args">Command-line arguments (unused).</param>
/// <returns>0 on success, 2 on fatal error.</returns>
public static int Main(string[] args)
{
Log.Logger = new LoggerConfiguration()