Initial commit: managed .NET 10 AVEVA Historian SDK + reverse-engineering toolkit
Full read-only SDK (src/AVEVA.Historian.Client) implementing the CLAUDE.md required
surface against AVEVA Historian's binary WCF protocol — no native AVEVA runtime
dependency. All operations live-verified against a local Historian:
- ProbeAsync, ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadEventsAsync
- BrowseTagNamesAsync, GetTagMetadataAsync (17 native data-type codes mapped)
- GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync
- 108/108 unit + integration tests pass
Includes the reverse-engineering toolkit (tools/AVEVA.Historian.ReverseEngineering)
used to decode the protocol: WCF probes, IL inspection via dnlib, and IL-rewrite
instrumentation (instrument-wcf-{write,read}message etc.) plus the .NET Framework
trace harness (tools/AVEVA.Historian.NativeTraceHarness) for parity testing.
Sanitized handoff evidence under docs/reverse-engineering/. Native AVEVA binaries
(current/, aveva-install-x64/, aveva-install-x86/) are gitignored — fetch separately
from the AVEVA installer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
using AVEVA.Historian.Client.Models;
|
||||
using AVEVA.Historian.Client.Protocol;
|
||||
using AVEVA.Historian.Client.Transport;
|
||||
using AVEVA.Historian.Client.Wcf;
|
||||
|
||||
namespace AVEVA.Historian.Client;
|
||||
|
||||
public sealed class HistorianClient : IAsyncDisposable
|
||||
{
|
||||
private readonly HistorianClientOptions _options;
|
||||
private readonly IHistorianTransportFactory _transportFactory;
|
||||
private readonly Historian2020ProtocolDialect _protocol;
|
||||
|
||||
public HistorianClient(HistorianClientOptions options)
|
||||
: this(options, TcpHistorianTransport.Factory)
|
||||
{
|
||||
}
|
||||
|
||||
internal HistorianClient(HistorianClientOptions options, IHistorianTransportFactory transportFactory)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_transportFactory = transportFactory ?? throw new ArgumentNullException(nameof(transportFactory));
|
||||
_protocol = new Historian2020ProtocolDialect(_options);
|
||||
}
|
||||
|
||||
public async Task<bool> ProbeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await HistorianWcfProbe.ProbeAsync(_options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<HistorianSample> ReadRawAsync(
|
||||
string tag,
|
||||
DateTime startUtc,
|
||||
DateTime endUtc,
|
||||
int maxValues,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxValues);
|
||||
ValidateTimeRange(startUtc, endUtc);
|
||||
|
||||
return _protocol.ReadRawAsync(tag, startUtc, endUtc, maxValues, cancellationToken);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<HistorianAggregateSample> ReadAggregateAsync(
|
||||
string tag,
|
||||
DateTime startUtc,
|
||||
DateTime endUtc,
|
||||
RetrievalMode mode,
|
||||
TimeSpan interval,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(interval, TimeSpan.Zero);
|
||||
ValidateTimeRange(startUtc, endUtc);
|
||||
|
||||
return _protocol.ReadAggregateAsync(tag, startUtc, endUtc, mode, interval, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<HistorianSample>> ReadAtTimeAsync(
|
||||
string tag,
|
||||
IReadOnlyList<DateTime> timestampsUtc,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
|
||||
ArgumentNullException.ThrowIfNull(timestampsUtc);
|
||||
|
||||
if (timestampsUtc.Count == 0)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<HistorianSample>>(Array.Empty<HistorianSample>());
|
||||
}
|
||||
|
||||
return _protocol.ReadAtTimeAsync(tag, timestampsUtc, cancellationToken);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<HistorianBlock> ReadBlocksAsync(
|
||||
string tag,
|
||||
DateTime startUtc,
|
||||
DateTime endUtc,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
|
||||
ValidateTimeRange(startUtc, endUtc);
|
||||
return _protocol.ReadBlocksAsync(tag, startUtc, endUtc, cancellationToken);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<HistorianEvent> ReadEventsAsync(
|
||||
DateTime startUtc,
|
||||
DateTime endUtc,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ValidateTimeRange(startUtc, endUtc);
|
||||
return _protocol.ReadEventsAsync(startUtc, endUtc, cancellationToken);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<string> BrowseTagNamesAsync(string filter = "*", CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(filter);
|
||||
return HistorianWcfTagClient.BrowseTagNamesAsync(_options, filter, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<HistorianTagMetadata?> GetTagMetadataAsync(string tag, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
|
||||
return HistorianWcfTagClient.GetTagMetadataAsync(_options, tag, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<HistorianConnectionStatus> GetConnectionStatusAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _protocol.GetConnectionStatusAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task<HistorianStoreForwardStatus> GetStoreForwardStatusAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _protocol.GetStoreForwardStatusAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task<string?> GetSystemParameterAsync(string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
return _protocol.GetSystemParameterAsync(name, cancellationToken);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private static void ValidateTimeRange(DateTime startUtc, DateTime endUtc)
|
||||
{
|
||||
if (startUtc.ToUniversalTime() > endUtc.ToUniversalTime())
|
||||
{
|
||||
throw new ArgumentException("Start time must be less than or equal to end time.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user