c19d124e89
Cheap-and-fast probe: open TCP socket to the configured endpoint, close immediately. Surfaces SocketError on failure, latency on success, "timed out" on caller cancel. Sufficient for the AdminUI Test Connect "can we reach the host?" question. Richer protocol- level probes (OPC UA session open, FOCAS handshake, gRPC ping) are a documented follow-up. Each probe registered as AddSingleton<IDriverProbe, X> in DriverFactoryBootstrap so they flow through DI into AdminOperationsActor. Historian.Wonderware returns a clean "TCP probe not applicable" result because it communicates over a Windows named pipe, not TCP. Also adds OpcUaClient + Historian.Wonderware.Client project references to Host.csproj (both were missing from the driver ItemGroup).
64 lines
2.3 KiB
C#
64 lines
2.3 KiB
C#
using System.Diagnostics;
|
|
using System.Net.Sockets;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
|
|
|
/// <summary>
|
|
/// Cheap TCP-connect probe for the <see cref="ModbusDriverOptions"/>-shaped driver config.
|
|
/// Opens a socket to the configured endpoint and closes immediately. Surfaces a green
|
|
/// tick + latency on success; red chip + SocketError on failure; "timed out" on the
|
|
/// caller's cancellation. Does NOT exchange any protocol bytes — richer per-driver
|
|
/// handshakes are a documented follow-up.
|
|
/// </summary>
|
|
public sealed class ModbusDriverProbe : IDriverProbe
|
|
{
|
|
private static readonly JsonSerializerOptions _opts = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
|
|
};
|
|
|
|
/// <inheritdoc />
|
|
public string DriverType => "Modbus";
|
|
|
|
/// <inheritdoc />
|
|
public async Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct)
|
|
{
|
|
ModbusDriverOptions? opts;
|
|
try { opts = JsonSerializer.Deserialize<ModbusDriverOptions>(configJson, _opts); }
|
|
catch (Exception ex) { return new(false, $"Config JSON is invalid: {ex.Message}", null); }
|
|
if (opts is null) return new(false, "Config JSON deserialized to null.", null);
|
|
|
|
var (host, port) = ExtractTarget(opts);
|
|
if (string.IsNullOrWhiteSpace(host) || port <= 0)
|
|
return new(false, "Config has no host/port to probe.", null);
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
try
|
|
{
|
|
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await socket.ConnectAsync(host, port, ct);
|
|
sw.Stop();
|
|
return new(true, null, sw.Elapsed);
|
|
}
|
|
catch (SocketException ex)
|
|
{
|
|
return new(false, $"Connect failed: {ex.SocketErrorCode}", null);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new(false, ex.Message, null);
|
|
}
|
|
}
|
|
|
|
private static (string host, int port) ExtractTarget(ModbusDriverOptions opts)
|
|
=> (opts.Host, opts.Port);
|
|
}
|