using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Junction.Domain;
using Junction.Domain.Models;
using Junction.Domain.Protocols;
namespace Junction.Protocols.OpcUa
{
///
/// Per-machine OPC UA protocol driver. Reads the configured node values and browses the address
/// space through an injected (the transport seam, mirroring the HttpClient
/// injection in MtconnectDriver). SDK types stay behind so this
/// driver is unit-tested against a mock with no live server.
///
/// Never throws for expected transport failures: maps them to
/// and honours cancellation via .
///
///
public sealed class OpcuaDriver : IProtocolDriver
{
private const string Source = "OpcuaDriver";
private readonly IUaClient _client;
private readonly Guid _machineId;
private readonly IReadOnlyList _nodeIds;
private readonly string _browseRoot;
private readonly TimeSpan _timeout;
// Selected data item ids to keep in ReadCurrentAsync snapshots. Empty => opt-in "monitor nothing".
private readonly HashSet _monitoredItemIds;
/// Protocol identifier this driver serves.
public string ProtocolId => "opcua";
/// Transport seam; injected for testability, owned by the factory.
/// Machine this driver reads for; stamped onto the snapshot.
/// Configured node ids to read in .
/// Address-space browse start node for .
/// Per-operation timeout enforced via a linked .
///
/// DataItem ids the user selected to monitor. keeps only these
/// (opt-in); empty/null => "monitor nothing".
///
public OpcuaDriver(
IUaClient client,
Guid machineId,
IReadOnlyList? nodeIds,
string browseRoot,
TimeSpan timeout,
IReadOnlyCollection? monitoredItemIds = null)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_machineId = machineId;
_nodeIds = nodeIds ?? Array.Empty();
_browseRoot = browseRoot ?? "";
if (timeout <= TimeSpan.Zero && timeout != Timeout.InfiniteTimeSpan)
{
throw new ArgumentOutOfRangeException(nameof(timeout), timeout, "Timeout must be positive or Timeout.InfiniteTimeSpan.");
}
_timeout = timeout;
_monitoredItemIds = new HashSet(StringComparer.Ordinal);
if (monitoredItemIds != null)
{
foreach (var id in monitoredItemIds)
{
if (!string.IsNullOrEmpty(id))
{
_monitoredItemIds.Add(id);
}
}
}
}
///
public async Task> ReadCurrentAsync(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Result.Cancelled();
}
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(_timeout);
IReadOnlyList reads;
try
{
await _client.ConnectAsync(cts.Token).ConfigureAwait(false);
reads = await _client.ReadAsync(_nodeIds, cts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return Result.Cancelled();
}
catch (OperationCanceledException ex)
{
return Fail("CURRENT_TIMEOUT", "OPC UA read timed out: " + ex.Message);
}
catch (Exception ex)
{
return Fail("CURRENT_READ_ERROR", "OPC UA read failed: " + ex.Message);
}
var items = new List(reads.Count);
foreach (UaReadResult r in reads)
{
items.Add(new DataItem(r.NodeId, r.NodeId, r.Value, "VARIABLE", r.SourceTimestamp));
}
var snapshot = new MachineSnapshot(_machineId, DateTimeOffset.UtcNow, ConnectionState.Connected, items);
return Result.Ok(FilterToMonitored(snapshot));
}
///
public async Task>> ProbeAsync(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Result>.Cancelled();
}
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(_timeout);
IReadOnlyList descriptors;
try
{
await _client.ConnectAsync(cts.Token).ConfigureAwait(false);
descriptors = await _client.BrowseAsync(_browseRoot, cts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return Result>.Cancelled();
}
catch (OperationCanceledException ex)
{
return ProbeFail("PROBE_TIMEOUT", "OPC UA browse timed out: " + ex.Message);
}
catch (Exception ex)
{
return ProbeFail("PROBE_BROWSE_ERROR", "OPC UA browse failed: " + ex.Message);
}
return Result>.Ok(descriptors);
}
///
/// Returns a copy of keeping only items whose id is in the monitored
/// selection. Empty selection yields an empty item set (opt-in). Connection state and capture
/// instant are preserved. Exact semantics as MtconnectDriver.
///
private MachineSnapshot FilterToMonitored(MachineSnapshot snapshot)
{
if (_monitoredItemIds.Count == 0)
{
return new MachineSnapshot(
snapshot.MachineId, snapshot.CapturedAt, snapshot.ConnectionState, Array.Empty());
}
var kept = new List();
for (int i = 0; i < snapshot.Items.Count; i++)
{
DataItem item = snapshot.Items[i];
if (_monitoredItemIds.Contains(item.Id))
{
kept.Add(item);
}
}
return new MachineSnapshot(snapshot.MachineId, snapshot.CapturedAt, snapshot.ConnectionState, kept);
}
private static Result Fail(string code, string message) =>
Result.Fail(new OperationError(code, Source, message));
private static Result> ProbeFail(string code, string message) =>
Result>.Fail(new OperationError(code, Source, message));
}
}