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:
dohertj2
2026-05-04 06:31:48 -04:00
commit c95824a65d
230 changed files with 38666 additions and 0 deletions
@@ -0,0 +1,167 @@
using System.Net.Security;
using System.Runtime.Versioning;
using System.ServiceModel;
using System.ServiceModel.Channels;
namespace AVEVA.Historian.Client.Wcf;
internal static class HistorianWcfBindingFactory
{
public const string Scheme = "net.tcp";
public const int DefaultPort = 32568;
public static Binding CreateMdasNetTcpBinding(TimeSpan timeout, long maxReceivedMessageSize = 64 * 1024 * 1024)
{
var encoding = new MdasMessageEncodingBindingElement(
new BinaryMessageEncodingBindingElement
{
MessageVersion = MessageVersion.Soap12WSAddressing10
});
var transport = new TcpTransportBindingElement
{
MaxReceivedMessageSize = maxReceivedMessageSize,
TransferMode = TransferMode.Buffered
};
return new CustomBinding(encoding, transport)
{
CloseTimeout = timeout,
OpenTimeout = timeout,
ReceiveTimeout = timeout,
SendTimeout = timeout
};
}
public static Binding CreateMdasNetTcpWindowsBinding(TimeSpan timeout, long maxReceivedMessageSize = 64 * 1024 * 1024)
{
NetTcpBinding nativeShape = new(SecurityMode.Transport)
{
MaxReceivedMessageSize = maxReceivedMessageSize,
MaxBufferSize = checked((int)Math.Min(maxReceivedMessageSize, int.MaxValue))
};
nativeShape.ReaderQuotas.MaxArrayLength = nativeShape.MaxBufferSize;
nativeShape.Security.Transport.ClientCredentialType = TcpClientCredentialType.Windows;
nativeShape.Security.Transport.ProtectionLevel = ProtectionLevel.None;
BindingElementCollection elements = nativeShape.CreateBindingElements();
for (int i = 0; i < elements.Count; i++)
{
if (elements[i] is MessageEncodingBindingElement encoding)
{
elements[i] = new MdasMessageEncodingBindingElement(encoding);
break;
}
}
return new CustomBinding(elements)
{
CloseTimeout = timeout,
OpenTimeout = timeout,
ReceiveTimeout = timeout,
SendTimeout = timeout
};
}
public static Binding CreateMdasNetTcpCertificateBinding(TimeSpan timeout, long maxReceivedMessageSize = 64 * 1024 * 1024)
{
NetTcpBinding nativeShape = new(SecurityMode.Transport)
{
MaxReceivedMessageSize = maxReceivedMessageSize,
MaxBufferSize = checked((int)Math.Min(maxReceivedMessageSize, int.MaxValue))
};
nativeShape.ReaderQuotas.MaxArrayLength = nativeShape.MaxBufferSize;
nativeShape.Security.Transport.ClientCredentialType = TcpClientCredentialType.None;
BindingElementCollection elements = nativeShape.CreateBindingElements();
for (int i = 0; i < elements.Count; i++)
{
if (elements[i] is MessageEncodingBindingElement encoding)
{
elements[i] = new MdasMessageEncodingBindingElement(encoding);
break;
}
}
return new CustomBinding(elements)
{
CloseTimeout = timeout,
OpenTimeout = timeout,
ReceiveTimeout = timeout,
SendTimeout = timeout
};
}
[SupportedOSPlatform("windows")]
public static Binding CreateMdasNetNamedPipeBinding(TimeSpan timeout, int maxBufferSize = 64 * 1024 * 1024)
{
NetNamedPipeBinding nativeShape = new()
{
MaxBufferSize = maxBufferSize,
MaxReceivedMessageSize = maxBufferSize
};
nativeShape.Security.Mode = NetNamedPipeSecurityMode.None;
nativeShape.ReaderQuotas.MaxArrayLength = maxBufferSize;
BindingElementCollection elements = nativeShape.CreateBindingElements();
for (int i = 0; i < elements.Count; i++)
{
if (elements[i] is MessageEncodingBindingElement encoding)
{
elements[i] = new MdasMessageEncodingBindingElement(encoding);
break;
}
}
return new CustomBinding(elements)
{
CloseTimeout = timeout,
OpenTimeout = timeout,
ReceiveTimeout = timeout,
SendTimeout = timeout
};
}
[SupportedOSPlatform("windows")]
public static (Binding HistoryBinding, EndpointAddress HistoryEndpoint, Binding RetrievalBinding, EndpointAddress RetrievalEndpoint) CreateBindingPair(
HistorianClientOptions options)
{
TimeSpan timeout = options.RequestTimeout;
return options.Transport switch
{
HistorianTransport.LocalPipe => (
CreateMdasNetNamedPipeBinding(timeout),
CreatePipeEndpointAddress(options.Host, HistorianWcfServiceNames.History),
CreateMdasNetNamedPipeBinding(timeout),
CreatePipeEndpointAddress(options.Host, HistorianWcfServiceNames.Retrieval)),
HistorianTransport.RemoteTcpIntegrated => (
CreateMdasNetTcpWindowsBinding(timeout),
CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.HistoryIntegrated),
CreateMdasNetTcpBinding(timeout),
CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.Retrieval)),
HistorianTransport.RemoteTcpCertificate => (
CreateMdasNetTcpCertificateBinding(timeout),
CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.HistoryCertificate),
CreateMdasNetTcpBinding(timeout),
CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.Retrieval)),
_ => throw new NotSupportedException($"Transport {options.Transport} is not supported.")
};
}
public static EndpointAddress CreateEndpointAddress(string host, int port, string serviceName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(host);
ArgumentException.ThrowIfNullOrWhiteSpace(serviceName);
return new EndpointAddress($"{Scheme}://{host}:{port}/{serviceName}");
}
public static EndpointAddress CreatePipeEndpointAddress(string host, string serviceName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(host);
ArgumentException.ThrowIfNullOrWhiteSpace(serviceName);
return new EndpointAddress($"net.pipe://{host}/{serviceName}");
}
}