Files
histsdk/src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs
T
Joseph Doherty 3525653c2b fix(grpc): extended-property read parser + GetConnectionStatus over gRPC
- HistorianTagExtendedPropertyProtocol.ParseResponse: fix the multi-property/
  multi-group response shape captured live from the 2023 R2 server. The server
  returns one group per property (the tag name repeats), each propertyCount=1, and
  a uint16 searchability-flags trailer per property (0x0003 built-in, 0x0001 user-
  added) — NOT the single-byte group trailer the old model assumed, which drifted
  one byte per group and threw "expected 0x09 found 0x01" on any buffer with more
  than one property. Now reads the per-property uint16 trailer (tolerates a legacy
  1-byte tail). Fixes read-back on both WCF and gRPC. Adds GetTagExtendedPropertiesRaw
  for future captures.
- HistorianGrpcStatusClient.GetConnectionStatusAsync (plan #5): synthesize connection
  status from a measured gRPC handshake (OpenConnection yielding a storage-session
  GUID => connected), mirroring the WCF synthesize-from-probe approach. Routed in
  Historian2020ProtocolDialect on UseGrpc (the WCF path used the MDAS binding, which
  can't reach the gRPC port).
- HistorianGrpcSqlClient: record the negative plan-#4 result — a HistoryService.
  RegisterTags prime does NOT clear the server-side CSrvDbConnection fault (tried live
  on both 0x402/0x401); the op stays bounded behind ProtocolEvidenceMissingException.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
2026-06-22 06:03:38 -04:00

139 lines
6.9 KiB
C#

using AVEVA.Historian.Client.Grpc;
using AVEVA.Historian.Client.Models;
using AVEVA.Historian.Client.Wcf;
namespace AVEVA.Historian.Client.Protocol;
internal sealed class Historian2020ProtocolDialect
{
private readonly HistorianClientOptions _options;
public Historian2020ProtocolDialect(HistorianClientOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
}
private bool UseGrpc => _options.Transport == HistorianTransport.RemoteGrpc;
public IAsyncEnumerable<HistorianSample> ReadRawAsync(string tag, DateTime startUtc, DateTime endUtc, int maxValues, CancellationToken cancellationToken)
{
return UseGrpc
? new HistorianGrpcReadOrchestrator(_options).ReadRawAsync(tag, startUtc, endUtc, maxValues, cancellationToken)
: new HistorianWcfReadOrchestrator(_options).ReadRawAsync(tag, startUtc, endUtc, maxValues, cancellationToken);
}
public IAsyncEnumerable<HistorianAggregateSample> ReadAggregateAsync(string tag, DateTime startUtc, DateTime endUtc, RetrievalMode mode, TimeSpan interval, CancellationToken cancellationToken)
{
return UseGrpc
? new HistorianGrpcReadOrchestrator(_options).ReadAggregateAsync(tag, startUtc, endUtc, mode, interval, cancellationToken)
: new HistorianWcfReadOrchestrator(_options).ReadAggregateAsync(tag, startUtc, endUtc, mode, interval, cancellationToken);
}
public Task<IReadOnlyList<HistorianSample>> ReadAtTimeAsync(string tag, IReadOnlyList<DateTime> timestampsUtc, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return UseGrpc
? new HistorianGrpcReadOrchestrator(_options).ReadAtTimeAsync(tag, timestampsUtc, cancellationToken)
: new HistorianWcfReadOrchestrator(_options).ReadAtTimeAsync(tag, timestampsUtc, cancellationToken);
}
public IAsyncEnumerable<HistorianBlock> ReadBlocksAsync(string tag, DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken)
{
return Missing<HistorianBlock>("StartBlockRetrievalQuery", cancellationToken);
}
public IAsyncEnumerable<HistorianEvent> ReadEventsAsync(DateTime startUtc, DateTime endUtc, HistorianEventFilter? filter, CancellationToken cancellationToken)
{
return UseGrpc
? new HistorianGrpcEventOrchestrator(_options).ReadEventsAsync(startUtc, endUtc, filter, cancellationToken)
: new HistorianWcfEventOrchestrator(_options).ReadEventsAsync(startUtc, endUtc, filter, cancellationToken);
}
public Task<HistorianConnectionStatus> GetConnectionStatusAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// Over gRPC (2023 R2) the status is measured from the gRPC handshake (the WCF synthesize-from-
// probe path uses the MDAS binding, which can't reach the gRPC port). Non-gRPC stays on WCF.
return UseGrpc
? HistorianGrpcStatusClient.GetConnectionStatusAsync(_options, cancellationToken)
: Wcf.HistorianWcfStatusClient.GetConnectionStatusAsync(_options, cancellationToken);
}
public Task<HistorianStoreForwardStatus> GetStoreForwardStatusAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// Over gRPC (2023 R2) we return a MEASURED idle-state: the client actually contacts the server
// (GetHistorianConsoleStatus) and reports ErrorOccurred when unreachable. The active-SF buffer
// magnitude lives behind the D2 storage-engine console wall and stays false. Non-gRPC transports
// keep the synthesized all-false (no SF sidecar to probe). See R4.3 §9.7.
return UseGrpc
? HistorianGrpcStatusClient.GetStoreForwardStatusAsync(_options, cancellationToken)
: Wcf.HistorianWcfStatusClient.GetStoreForwardStatusAsync(_options, cancellationToken);
}
public Task<string?> GetSystemParameterAsync(string name, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ArgumentException.ThrowIfNullOrWhiteSpace(name);
return UseGrpc
? HistorianGrpcStatusClient.GetSystemParameterAsync(_options, name, cancellationToken)
: Wcf.HistorianWcfStatusClient.GetSystemParameterAsync(_options, name, cancellationToken);
}
public Task<string?> GetServerTimeZoneAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// 2023 R2 gRPC returns the real server time-zone name; the 2020 WCF
// GetSystemTimeZoneName is a client-side stub (empty value), so there is no evidence-backed
// value to return on that transport — fail closed rather than hand back an empty string.
if (!UseGrpc)
{
throw new ProtocolEvidenceMissingException("GetSystemTimeZoneName (2020 WCF stub — gRPC/2023R2 only)");
}
return HistorianGrpcStatusClient.GetSystemTimeZoneNameAsync(_options, cancellationToken);
}
public Task<string?> GetRuntimeParameterAsync(string name, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ArgumentException.ThrowIfNullOrWhiteSpace(name);
return UseGrpc
? HistorianGrpcStatusClient.GetRuntimeParameterAsync(_options, name, cancellationToken)
: Wcf.HistorianWcfStatusClient.GetRuntimeParameterAsync(_options, name, cancellationToken);
}
public Task<IReadOnlyList<Models.HistorianTagExtendedProperty>> GetTagExtendedPropertiesAsync(string tag, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
return UseGrpc
? Grpc.HistorianGrpcTagClient.GetTagExtendedPropertiesAsync(_options, tag, cancellationToken)
: Wcf.HistorianWcfTagExtendedPropertyClient.GetTagExtendedPropertiesAsync(_options, tag, cancellationToken);
}
public Task<HistorianSqlResult> ExecuteSqlCommandAsync(string command, HistorianSqlExecuteOption option, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ArgumentException.ThrowIfNullOrWhiteSpace(command);
return UseGrpc
? Grpc.HistorianGrpcSqlClient.ExecuteSqlCommandAsync(_options, command, option, cancellationToken)
: Wcf.HistorianWcfSqlClient.ExecuteSqlCommandAsync(_options, command, option, cancellationToken);
}
private static async IAsyncEnumerable<T> Missing<T>(
string operation,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.Yield();
cancellationToken.ThrowIfCancellationRequested();
throw new ProtocolEvidenceMissingException(operation);
#pragma warning disable CS0162
yield break;
#pragma warning restore CS0162
}
}