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; /// /// Modbus FC03 probe for the -shaped driver config. /// Opens a TCP connection to the configured endpoint and sends a one-shot FC03 /// (Read Holding Registers, qty 1 @ address 0) handshake. A normal FC03 response /// or a Modbus exception PDU both confirm a live Modbus device (green + latency); /// TCP failure surfaces the SocketError; a Modbus-level handshake failure after /// TCP succeeds surfaces a targeted message; timeout surfaces "timed out after Ns." /// public sealed class ModbusDriverProbe : IDriverProbe { private static readonly JsonSerializerOptions _opts = new() { PropertyNameCaseInsensitive = true, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, }; // FC03 Read Holding Registers: function=0x03, addr-hi=0, addr-lo=0, qty-hi=0, qty-lo=1 private static readonly byte[] Fc03Pdu = [0x03, 0x00, 0x00, 0x00, 0x01]; /// public string DriverType => "Modbus"; /// public async Task ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct) { ModbusDriverOptions? opts; try { opts = JsonSerializer.Deserialize(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 unitId = opts.UnitId; var sw = Stopwatch.StartNew(); using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(timeout); // Phase 1 — TCP connect (using ModbusTcpTransport which handles IPv4 preference). // autoReconnect=false: this is a one-shot probe, no retry loops. var transport = new ModbusTcpTransport(host, port, timeout, autoReconnect: false); await using (transport.ConfigureAwait(false)) { try { await transport.ConnectAsync(cts.Token).ConfigureAwait(false); } 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); } // Phase 2 — FC03 handshake. TCP is up; now prove a Modbus device is answering. try { await transport.SendAsync(unitId, Fc03Pdu, cts.Token).ConfigureAwait(false); sw.Stop(); return new(true, "Modbus FC03 OK", sw.Elapsed); } catch (ModbusException) { // Device replied with an exception PDU — it IS a real Modbus device. sw.Stop(); return new(true, "Modbus FC03 OK (device returned exception PDU)", sw.Elapsed); } catch (OperationCanceledException) { return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null); } catch (Exception ex) { sw.Stop(); return new(false, $"Reachable at {host}:{port} but Modbus FC03 handshake failed: {ex.Message}", null); } } } private static (string host, int port) ExtractTarget(ModbusDriverOptions opts) => (opts.Host, opts.Port); }