using Google.Protobuf;
using AVEVA.Historian.Client.Models;
using AVEVA.Historian.Client.Wcf;
using GrpcHistory = ArchestrA.Grpc.Contract.History;
using GrpcRetrieval = ArchestrA.Grpc.Contract.Retrieval;
namespace AVEVA.Historian.Client.Grpc;
///
/// 2023 R2 gRPC orchestrator for the M3 historical (non-streamed original / backfill) value write.
/// Captured live from the native client (see docs/plans/revision-write-path.md §"R3.1
/// CAPTURED"): the historical write rides HistoryService.AddStreamValues with an "ON"
/// storage-sample buffer (), NOT the TransactionService
/// AddNonStreamValues path. The chain on a single write-enabled (0x401) session:
///
/// - OpenConnection (write-enabled) → string storage handle
/// - RetrievalService.GetTagInfosFromName → the per-tag GUID (parsed as the tag-info
/// record's TypeId) and registers the tag on the session
/// - HistoryService.AddStreamValues(strHandle, "ON" buffer) per sample
///
/// The tag must already exist (create it with EnsureTagAsync first). Only the Float value
/// encoding is captured; other tag types are rejected by the serializer until captured.
///
internal sealed class HistorianGrpcHistoricalWriteOrchestrator
{
private readonly HistorianClientOptions _options;
public HistorianGrpcHistoricalWriteOrchestrator(HistorianClientOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
}
public Task AddHistoricalValuesAsync(
string tag,
IReadOnlyList values,
CancellationToken cancellationToken)
=> Task.Run(() => Run(tag, values, cancellationToken), cancellationToken);
private bool Run(string tag, IReadOnlyList values, CancellationToken cancellationToken)
{
if (values.Count == 0)
{
return true;
}
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options);
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(
connection, _options, cancellationToken,
connectionMode: HistorianWcfAuthChainHelper.NativeIntegratedWriteEnabledConnectionMode);
string handle = session.StringHandle;
DateTime Deadline() => DateTime.UtcNow.Add(_options.RequestTimeout);
// Resolve the per-tag GUID (and register the tag on this write session) via
// GetTagInfosFromName. The 16-byte GUID the "ON" buffer needs is the tag-info record's TypeId.
var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel);
GrpcRetrieval.GetTagInfosFromNameResponse tagInfoResponse = retrievalClient.GetTagInfosFromName(
new GrpcRetrieval.GetTagInfosFromNameRequest
{
StrHandle = handle,
BtTagNames = ByteString.CopyFrom(HistorianGrpcTagClient.BuildTagNamesBuffer([tag])),
UiSequence = 0,
},
connection.Metadata, Deadline(), cancellationToken);
if (!(tagInfoResponse.Status?.BSuccess ?? false))
{
byte[] error = tagInfoResponse.Status?.BtError?.ToByteArray() ?? [];
throw new InvalidOperationException(
$"gRPC GetTagInfosFromName failed for tag '{tag}' (errorLen={error.Length}); does the tag exist?");
}
byte[] tagInfos = tagInfoResponse.BtTagInfos?.ToByteArray() ?? [];
IReadOnlyList parsed = HistorianTagQueryProtocol.ParseGetTagInfoResponse(tagInfos);
if (parsed.Count == 0)
{
throw new InvalidOperationException($"Tag '{tag}' not found on the server.");
}
Guid tagGuid = parsed[0].TypeId;
HistorianDataType dataType = HistorianWcfTagClient.MapDataType(parsed[0].NativeDataTypeDescriptor);
var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel);
foreach (HistorianHistoricalValue value in values)
{
cancellationToken.ThrowIfCancellationRequested();
byte[] buffer = HistorianHistoricalWriteProtocol.SerializeAddStreamValuesBuffer(
tagGuid,
dataType,
value.TimestampUtc,
value.Value,
DateTime.UtcNow,
value.OpcQuality);
GrpcHistory.AddStreamValuesResponse response = historyClient.AddStreamValues(
new GrpcHistory.AddStreamValuesRequest
{
StrHandle = handle,
BtValues = ByteString.CopyFrom(buffer),
},
connection.Metadata, Deadline(), cancellationToken);
if (!(response.Status?.BSuccess ?? false))
{
byte[] error = response.Status?.BtError?.ToByteArray() ?? [];
throw new InvalidOperationException(
$"gRPC AddStreamValues failed for tag '{tag}' (errorLen={error.Length}).");
}
}
return true;
}
}