using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.MxGateway.Client; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; /// /// Driver-side wrapper around the gateway's . Owns the /// MXAccess Register handle, caches the per-tag item handles AddItem returns, /// and coordinates the read / write / subscribe call paths. PRs 4.2-4.5 fill this in /// incrementally: /// /// PR 4.2 (this PR) — skeleton + lifecycle wiring. /// PR 4.3 — write path. /// PR 4.4 — subscription registry + event pump + the production /// implementation that drives the read path. /// PR 4.5 — reconnect supervisor. /// /// public sealed class GalaxyMxSession : IAsyncDisposable { private readonly GalaxyMxAccessOptions _options; private readonly ILogger _logger; // Owned gateway client + session — populated when ConnectAsync runs. Tests can leave // them null and exercise the surface via injected IGalaxyDataReader fakes. private MxGatewayClient? _ownedClient; private MxGatewaySession? _session; private int _serverHandle; private bool _disposed; public GalaxyMxSession(GalaxyMxAccessOptions options, ILogger? logger = null) { _options = options ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? NullLogger.Instance; } public bool IsConnected => _session is not null; /// /// Server-side handle returned by MXAccess Register. Zero before /// opens the session. /// public int ServerHandle => _serverHandle; /// /// Connect the underlying gateway client + open an MXAccess session + register the /// configured client name. Idempotent — second calls are no-ops while /// is true. /// public async Task ConnectAsync(MxGatewayClientOptions clientOptions, CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_disposed, this); if (_session is not null) return; _ownedClient = MxGatewayClient.Create(clientOptions); _session = await _ownedClient.OpenSessionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); _serverHandle = await _session.RegisterAsync(_options.ClientName, cancellationToken).ConfigureAwait(false); _logger.LogInformation( "GalaxyMxSession connected — clientName={ClientName} serverHandle={Handle}", _options.ClientName, _serverHandle); } /// /// Test seam — attach a session opened externally (e.g. against an in-process gw /// fake). Skips the gateway-client construction so tests can drive the session /// surface without spinning a real gRPC channel. Caller retains client ownership. /// internal void AttachForTests(MxGatewaySession session, int serverHandle) { ObjectDisposedException.ThrowIf(_disposed, this); _session = session ?? throw new ArgumentNullException(nameof(session)); _serverHandle = serverHandle; } /// /// Returns the underlying gateway session. Null until or /// runs. PR 4.3 / 4.4 use this to issue commands. /// public MxGatewaySession? Session => _session; public async ValueTask DisposeAsync() { if (_disposed) return; _disposed = true; if (_session is not null) { try { await _session.DisposeAsync().ConfigureAwait(false); } catch (Exception ex) { _logger.LogWarning(ex, "GalaxyMxSession session dispose failed (best-effort)"); } } _session = null; if (_ownedClient is not null) { try { await _ownedClient.DisposeAsync().ConfigureAwait(false); } catch (Exception ex) { _logger.LogWarning(ex, "GalaxyMxSession client dispose failed (best-effort)"); } } _ownedClient = null; } }