chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+250
@@ -0,0 +1,250 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MessagePack;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Ipc;
|
||||
|
||||
/// <summary>
|
||||
/// Sidecar-side dispatcher. Each post-Hello frame routes by <see cref="MessageKind"/> to
|
||||
/// the right historian operation and the result frame is written back through the same
|
||||
/// pipe. Per-call exceptions are caught and surfaced as <c>Success=false, Error=...</c>
|
||||
/// replies so a single bad request doesn't kill the connection.
|
||||
/// </summary>
|
||||
public sealed class HistorianFrameHandler : IFrameHandler
|
||||
{
|
||||
private readonly IHistorianDataSource _historian;
|
||||
private readonly IAlarmEventWriter? _alarmWriter;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public HistorianFrameHandler(
|
||||
IHistorianDataSource historian,
|
||||
ILogger logger,
|
||||
IAlarmEventWriter? alarmWriter = null)
|
||||
{
|
||||
_historian = historian ?? throw new ArgumentNullException(nameof(historian));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_alarmWriter = alarmWriter;
|
||||
}
|
||||
|
||||
public Task HandleAsync(MessageKind kind, byte[] body, FrameWriter writer, CancellationToken ct)
|
||||
=> kind switch
|
||||
{
|
||||
MessageKind.ReadRawRequest => HandleReadRawAsync(body, writer, ct),
|
||||
MessageKind.ReadProcessedRequest => HandleReadProcessedAsync(body, writer, ct),
|
||||
MessageKind.ReadAtTimeRequest => HandleReadAtTimeAsync(body, writer, ct),
|
||||
MessageKind.ReadEventsRequest => HandleReadEventsAsync(body, writer, ct),
|
||||
MessageKind.WriteAlarmEventsRequest => HandleWriteAlarmEventsAsync(body, writer, ct),
|
||||
_ => UnknownAsync(kind),
|
||||
};
|
||||
|
||||
private Task UnknownAsync(MessageKind kind)
|
||||
{
|
||||
_logger.Warning("Sidecar received unsupported frame kind {Kind}; dropping", kind);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task HandleReadRawAsync(byte[] body, FrameWriter writer, CancellationToken ct)
|
||||
{
|
||||
var req = MessagePackSerializer.Deserialize<ReadRawRequest>(body);
|
||||
var reply = new ReadRawReply { CorrelationId = req.CorrelationId };
|
||||
try
|
||||
{
|
||||
var samples = await _historian.ReadRawAsync(
|
||||
req.TagName,
|
||||
new DateTime(req.StartUtcTicks, DateTimeKind.Utc),
|
||||
new DateTime(req.EndUtcTicks, DateTimeKind.Utc),
|
||||
req.MaxValues,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
reply.Success = true;
|
||||
reply.Samples = ToWire(samples);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Sidecar ReadRaw failed for {Tag}", req.TagName);
|
||||
reply.Success = false;
|
||||
reply.Error = ex.Message;
|
||||
}
|
||||
|
||||
await writer.WriteAsync(MessageKind.ReadRawReply, reply, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task HandleReadProcessedAsync(byte[] body, FrameWriter writer, CancellationToken ct)
|
||||
{
|
||||
var req = MessagePackSerializer.Deserialize<ReadProcessedRequest>(body);
|
||||
var reply = new ReadProcessedReply { CorrelationId = req.CorrelationId };
|
||||
try
|
||||
{
|
||||
var buckets = await _historian.ReadAggregateAsync(
|
||||
req.TagName,
|
||||
new DateTime(req.StartUtcTicks, DateTimeKind.Utc),
|
||||
new DateTime(req.EndUtcTicks, DateTimeKind.Utc),
|
||||
req.IntervalMs,
|
||||
req.AggregateColumn,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
reply.Success = true;
|
||||
reply.Buckets = ToWire(buckets);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Sidecar ReadProcessed failed for {Tag}", req.TagName);
|
||||
reply.Success = false;
|
||||
reply.Error = ex.Message;
|
||||
}
|
||||
|
||||
await writer.WriteAsync(MessageKind.ReadProcessedReply, reply, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task HandleReadAtTimeAsync(byte[] body, FrameWriter writer, CancellationToken ct)
|
||||
{
|
||||
var req = MessagePackSerializer.Deserialize<ReadAtTimeRequest>(body);
|
||||
var reply = new ReadAtTimeReply { CorrelationId = req.CorrelationId };
|
||||
try
|
||||
{
|
||||
var timestamps = new DateTime[req.TimestampsUtcTicks.Length];
|
||||
for (var i = 0; i < timestamps.Length; i++)
|
||||
timestamps[i] = new DateTime(req.TimestampsUtcTicks[i], DateTimeKind.Utc);
|
||||
|
||||
var samples = await _historian.ReadAtTimeAsync(req.TagName, timestamps, ct).ConfigureAwait(false);
|
||||
reply.Success = true;
|
||||
reply.Samples = ToWire(samples);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Sidecar ReadAtTime failed for {Tag}", req.TagName);
|
||||
reply.Success = false;
|
||||
reply.Error = ex.Message;
|
||||
}
|
||||
|
||||
await writer.WriteAsync(MessageKind.ReadAtTimeReply, reply, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task HandleReadEventsAsync(byte[] body, FrameWriter writer, CancellationToken ct)
|
||||
{
|
||||
var req = MessagePackSerializer.Deserialize<ReadEventsRequest>(body);
|
||||
var reply = new ReadEventsReply { CorrelationId = req.CorrelationId };
|
||||
try
|
||||
{
|
||||
var events = await _historian.ReadEventsAsync(
|
||||
req.SourceName,
|
||||
new DateTime(req.StartUtcTicks, DateTimeKind.Utc),
|
||||
new DateTime(req.EndUtcTicks, DateTimeKind.Utc),
|
||||
req.MaxEvents,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
reply.Success = true;
|
||||
reply.Events = ToWire(events);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Sidecar ReadEvents failed for source {Source}", req.SourceName);
|
||||
reply.Success = false;
|
||||
reply.Error = ex.Message;
|
||||
}
|
||||
|
||||
await writer.WriteAsync(MessageKind.ReadEventsReply, reply, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task HandleWriteAlarmEventsAsync(byte[] body, FrameWriter writer, CancellationToken ct)
|
||||
{
|
||||
var req = MessagePackSerializer.Deserialize<WriteAlarmEventsRequest>(body);
|
||||
var reply = new WriteAlarmEventsReply { CorrelationId = req.CorrelationId };
|
||||
|
||||
if (_alarmWriter is null)
|
||||
{
|
||||
reply.Success = false;
|
||||
reply.Error = "Sidecar not configured with an alarm-event writer.";
|
||||
reply.PerEventOk = new bool[req.Events.Length];
|
||||
await writer.WriteAsync(MessageKind.WriteAlarmEventsReply, reply, ct).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var perEvent = await _alarmWriter.WriteAsync(req.Events, ct).ConfigureAwait(false);
|
||||
reply.PerEventOk = perEvent;
|
||||
reply.Success = true;
|
||||
// Whole-batch Success stays true even when some events failed — per-event
|
||||
// PerEventOk slots carry the granular result; the SQLite drain worker treats
|
||||
// false slots as retry-please candidates.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Sidecar WriteAlarmEvents failed");
|
||||
reply.Success = false;
|
||||
reply.Error = ex.Message;
|
||||
reply.PerEventOk = new bool[req.Events.Length];
|
||||
}
|
||||
|
||||
await writer.WriteAsync(MessageKind.WriteAlarmEventsReply, reply, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static HistorianSampleDto[] ToWire(List<HistorianSample> samples)
|
||||
{
|
||||
var dtos = new HistorianSampleDto[samples.Count];
|
||||
for (var i = 0; i < samples.Count; i++)
|
||||
{
|
||||
var s = samples[i];
|
||||
dtos[i] = new HistorianSampleDto
|
||||
{
|
||||
ValueBytes = s.Value is null ? null : MessagePackSerializer.Serialize(s.Value),
|
||||
Quality = s.Quality,
|
||||
TimestampUtcTicks = s.TimestampUtc.Ticks,
|
||||
};
|
||||
}
|
||||
return dtos;
|
||||
}
|
||||
|
||||
private static HistorianAggregateSampleDto[] ToWire(List<HistorianAggregateSample> samples)
|
||||
{
|
||||
var dtos = new HistorianAggregateSampleDto[samples.Count];
|
||||
for (var i = 0; i < samples.Count; i++)
|
||||
{
|
||||
dtos[i] = new HistorianAggregateSampleDto
|
||||
{
|
||||
Value = samples[i].Value,
|
||||
TimestampUtcTicks = samples[i].TimestampUtc.Ticks,
|
||||
};
|
||||
}
|
||||
return dtos;
|
||||
}
|
||||
|
||||
private static HistorianEventDto[] ToWire(List<Backend.HistorianEventDto> events)
|
||||
{
|
||||
var dtos = new HistorianEventDto[events.Count];
|
||||
for (var i = 0; i < events.Count; i++)
|
||||
{
|
||||
var e = events[i];
|
||||
dtos[i] = new HistorianEventDto
|
||||
{
|
||||
EventId = e.Id.ToString(),
|
||||
Source = e.Source,
|
||||
EventTimeUtcTicks = e.EventTime.Ticks,
|
||||
ReceivedTimeUtcTicks = e.ReceivedTime.Ticks,
|
||||
DisplayText = e.DisplayText,
|
||||
Severity = e.Severity,
|
||||
};
|
||||
}
|
||||
return dtos;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strategy for persisting alarm events into the Wonderware Alarm & Events log. PR 3.W
|
||||
/// supplies a real implementation that drives the aahClient SDK; PR 3.3 ships the
|
||||
/// contract + a default null implementation so the sidecar can boot without one.
|
||||
/// </summary>
|
||||
public interface IAlarmEventWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a batch of alarm events. Returns one boolean per input event indicating
|
||||
/// persisted vs. retry-please. The SQLite store-and-forward sink retries failed
|
||||
/// slots on the next drain tick.
|
||||
/// </summary>
|
||||
Task<bool[]> WriteAsync(AlarmHistorianEventDto[] events, CancellationToken cancellationToken);
|
||||
}
|
||||
Reference in New Issue
Block a user