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:
Joseph Doherty
2026-05-17 01:55:28 -04:00
parent 69f02fed7f
commit a25593a9c6
1044 changed files with 365 additions and 343 deletions
@@ -0,0 +1,116 @@
using MxGateway.Client;
using MxGateway.Contracts.Proto;
// Use the generated nested status enum for the SetBufferedUpdateInterval reply check.
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
/// <summary>
/// Production <see cref="IGalaxySubscriber"/> over a connected
/// <see cref="GalaxyMxSession"/>. Forwards SubscribeBulk / UnsubscribeBulk to the
/// gateway and streams MxEvents via the gw's bidirectional events RPC.
/// </summary>
/// <remarks>
/// PR 6.3 wired the per-call <c>buffered_update_interval_ms</c> through
/// <see cref="SubscribeBulkAsync"/>. The gw's contract is session-level
/// (<c>SetBufferedUpdateInterval</c> applies to all buffered subscriptions on the
/// server handle), so we cache the last-applied value and skip redundant calls.
/// </remarks>
public sealed class GatewayGalaxySubscriber : IGalaxySubscriber
{
private readonly GalaxyMxSession _session;
private readonly Lock _intervalLock = new();
private int _lastAppliedIntervalMs = -1; // -1 = never applied; 0 = explicit "use gw default"
public GatewayGalaxySubscriber(GalaxyMxSession session)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
}
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
{
var session = _session.Session
?? throw new InvalidOperationException(
"GalaxyMxSession is not connected. Call ConnectAsync before subscribing.");
var serverHandle = _session.ServerHandle;
// The gw's SubscribeBulk RPC doesn't carry a per-call interval — buffered cadence
// is session-level, set via SetBufferedUpdateInterval. Apply it before the
// SubscribeBulk so the very first events on the new handles publish at the
// requested cadence. Skip when the last-applied value already matches.
if (bufferedUpdateIntervalMs > 0)
{
await EnsureSessionIntervalAsync(session, serverHandle, bufferedUpdateIntervalMs, cancellationToken)
.ConfigureAwait(false);
}
return await session.SubscribeBulkAsync(serverHandle, fullReferences, cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Apply the gateway's session-level <c>SetBufferedUpdateInterval</c> command. The
/// gw's contract is "for this server handle, every buffered subscription publishes
/// at this cadence" — there's no per-handle granularity, so we cache the last
/// applied value and skip redundant calls.
/// </summary>
private async Task EnsureSessionIntervalAsync(
MxGateway.Client.MxGatewaySession session, int serverHandle, int intervalMs, CancellationToken cancellationToken)
{
lock (_intervalLock)
{
if (_lastAppliedIntervalMs == intervalMs) return;
}
var reply = await session.InvokeAsync(
new MxCommandRequest
{
SessionId = session.SessionId,
ClientCorrelationId = Guid.NewGuid().ToString("N"),
Command = new MxCommand
{
Kind = MxCommandKind.SetBufferedUpdateInterval,
SetBufferedUpdateInterval = new SetBufferedUpdateIntervalCommand
{
ServerHandle = serverHandle,
UpdateIntervalMilliseconds = intervalMs,
},
},
},
cancellationToken).ConfigureAwait(false);
if (reply.ProtocolStatus?.Code is not (ProtocolStatusCode.Ok or ProtocolStatusCode.MxaccessFailure))
{
// Don't throw on a soft failure — the SubscribeBulk will still succeed at the
// gw's default cadence, which is functional just not the requested cadence.
// The trace span (PR 6.1) plus the warning here gives ops the signal.
return;
}
lock (_intervalLock)
{
_lastAppliedIntervalMs = intervalMs;
}
}
public async Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
{
if (itemHandles.Count == 0) return;
var session = _session.Session
?? throw new InvalidOperationException(
"GalaxyMxSession is not connected. UnsubscribeBulk called after disconnect.");
var serverHandle = _session.ServerHandle;
await session.UnsubscribeBulkAsync(serverHandle, itemHandles, cancellationToken)
.ConfigureAwait(false);
}
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
{
var session = _session.Session
?? throw new InvalidOperationException(
"GalaxyMxSession is not connected. StreamEventsAsync called before ConnectAsync.");
return session.StreamEventsAsync(afterWorkerSequence: 0, cancellationToken);
}
}