using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Ipc; /// /// Phase 7 follow-up (task #247) — bridges 's /// drain worker to Driver.Galaxy.Host over the existing /// pipe. Translates batches into the /// wire format the Host expects + maps per-event /// responses back to /// so the SQLite queue knows what to ack / /// dead-letter / retry. /// /// /// /// Reuses the IPC channel already opens for the /// Galaxy data plane — no second pipe to Driver.Galaxy.Host, no separate /// auth handshake. The IPC client's call gate serializes historian batches with /// driver Reads/Writes/Subscribes; historian batches are infrequent (every few /// seconds at most under the SQLite sink's drain cadence) so the contention is /// negligible compared to per-tag-read pressure. /// /// /// Pipe-level transport faults (broken pipe, host crash) bubble up as /// which the SQLite sink's drain worker catches + /// translates to a whole-batch RetryPlease per the /// docstring — failed events stay queued /// for the next drain tick after backoff. /// /// public sealed class GalaxyHistorianWriter : IAlarmHistorianWriter { private readonly GalaxyIpcClient _client; public GalaxyHistorianWriter(GalaxyIpcClient client) { _client = client ?? throw new ArgumentNullException(nameof(client)); } public async Task> WriteBatchAsync( IReadOnlyList batch, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(batch); if (batch.Count == 0) return []; var request = new HistorianAlarmEventRequest { Events = batch.Select(ToDto).ToArray(), }; var response = await _client.CallAsync( requestKind: MessageKind.HistorianAlarmEventRequest, request: request, expectedResponseKind: MessageKind.HistorianAlarmEventResponse, ct: cancellationToken).ConfigureAwait(false); if (response.Outcomes.Length != batch.Count) throw new InvalidOperationException( $"Galaxy.Host returned {response.Outcomes.Length} outcomes for a batch of {batch.Count} — protocol mismatch"); var outcomes = new HistorianWriteOutcome[response.Outcomes.Length]; for (var i = 0; i < response.Outcomes.Length; i++) outcomes[i] = MapOutcome(response.Outcomes[i]); return outcomes; } internal static HistorianAlarmEventDto ToDto(AlarmHistorianEvent e) => new() { AlarmId = e.AlarmId, EquipmentPath = e.EquipmentPath, AlarmName = e.AlarmName, AlarmTypeName = e.AlarmTypeName, Severity = (int)e.Severity, EventKind = e.EventKind, Message = e.Message, User = e.User, Comment = e.Comment, TimestampUtcUnixMs = new DateTimeOffset(e.TimestampUtc, TimeSpan.Zero).ToUnixTimeMilliseconds(), }; internal static HistorianWriteOutcome MapOutcome(HistorianAlarmEventOutcomeDto wire) => wire switch { HistorianAlarmEventOutcomeDto.Ack => HistorianWriteOutcome.Ack, HistorianAlarmEventOutcomeDto.RetryPlease => HistorianWriteOutcome.RetryPlease, HistorianAlarmEventOutcomeDto.PermanentFail => HistorianWriteOutcome.PermanentFail, _ => throw new InvalidOperationException($"Unknown HistorianAlarmEventOutcomeDto byte {(byte)wire}"), }; }