using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; namespace ZB.MOM.WW.ScadaBridge.LoadHarness; /// /// In-process stand-in for an OPC UA / MxGateway server, registered on the REAL /// DataConnectionFactory under the protocol so the /// whole Data Connection Layer above it — DataConnectionManagerActor, /// DataConnectionActor, its _instancesByTag fan-out and the /// TagValueUpdate hand-off to Instance Actors — runs unmodified. /// /// /// It implements because the batch path is /// the one a real site takes at this scale; the per-tag fallback would make /// subscribe setup, not steady-state throughput, the thing being measured. /// /// /// Why faking here does not invalidate the measurement. Everything this class /// replaces is on the far side of the process boundary: socket I/O, the OPC UA SDK's /// own session/subscription machinery, and the device. The system under test — the /// actor hierarchy, the site stream, store-and-forward, health, and the streaming /// relay — begins at the invocation, which is /// exactly where the real adapter hands off. Driving 375,000 genuine monitored items /// would measure the OPC UA stack, not ScadaBridge. /// /// public sealed class SimulatedDataConnection : IDataConnection, IBatchSubscribableConnection { /// Protocol discriminator this adapter registers under on the factory. public const string ProtocolName = "LoadSim"; private SubscriptionCallback? _callback; private int _subscriptionCounter; /// /// The data connection name this adapter was created for, taken from the connection /// details. The DCL factory creates adapters as its manager actor processes the /// CreateConnectionCommands, so creation order is not connection order — the driver /// resolves an adapter by name rather than by index. /// public string ConnectionName { get; private set; } = string.Empty; /// Key under which the connection name travels in the connection details. public const string ConnectionNameKey = "connectionName"; /// public ConnectionHealth Status { get; private set; } = ConnectionHealth.Disconnected; /// public event Action? Disconnected; /// /// The callback captured at subscribe time. The tag driver invokes this to inject /// a value change, mirroring what the OPC UA SDK's notification thread does. /// Null until the site's Instance Actors have subscribed. /// public SubscriptionCallback? ValueCallback => _callback; /// Number of tag paths this connection has accepted subscriptions for. public int SubscribedTagCount => _subscriptionCounter; /// public Task ConnectAsync(IDictionary connectionDetails, CancellationToken cancellationToken = default) { if (connectionDetails.TryGetValue(ConnectionNameKey, out var name)) ConnectionName = name; Status = ConnectionHealth.Connected; return Task.CompletedTask; } /// public Task DisconnectAsync(CancellationToken cancellationToken = default) { Status = ConnectionHealth.Disconnected; Disconnected?.Invoke(); return Task.CompletedTask; } /// public Task SubscribeAsync(string tagPath, SubscriptionCallback callback, CancellationToken cancellationToken = default) { _callback = callback; return Task.FromResult($"sub-{Interlocked.Increment(ref _subscriptionCounter)}"); } /// public Task> SubscribeBatchAsync( IReadOnlyList tagPaths, SubscriptionCallback callback, CancellationToken cancellationToken = default) { _callback = callback; var results = new List(tagPaths.Count); foreach (var path in tagPaths) { results.Add(new TagSubscribeResult( path, true, $"sub-{Interlocked.Increment(ref _subscriptionCounter)}", null)); } return Task.FromResult>(results); } /// public Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default) => Task.CompletedTask; /// public Task UnsubscribeBatchAsync(IReadOnlyList subscriptionIds, CancellationToken cancellationToken = default) => Task.CompletedTask; /// public Task ReadAsync(string tagPath, CancellationToken cancellationToken = default) => Task.FromResult(new ReadResult(true, new TagValue(0d, QualityCode.Good, DateTimeOffset.UtcNow), null)); /// public Task> ReadBatchAsync( IEnumerable tagPaths, CancellationToken cancellationToken = default) { var now = DateTimeOffset.UtcNow; var results = new Dictionary(); foreach (var path in tagPaths) results[path] = new ReadResult(true, new TagValue(0d, QualityCode.Good, now), null); return Task.FromResult>(results); } /// public Task WriteAsync(string tagPath, object? value, CancellationToken cancellationToken = default) => Task.FromResult(new WriteResult(true, null)); /// public Task> WriteBatchAsync( IDictionary values, CancellationToken cancellationToken = default) { var results = new Dictionary(); foreach (var key in values.Keys) results[key] = new WriteResult(true, null); return Task.FromResult>(results); } /// public Task WriteBatchAndWaitAsync( IDictionary values, string flagPath, object? flagValue, string responsePath, object? responseValue, TimeSpan timeout, CancellationToken cancellationToken = default) => Task.FromResult(true); /// public ValueTask DisposeAsync() => ValueTask.CompletedTask; }