fix(driver-modbus): probe parses the factory DTO shape — timeoutMs no longer silently dropped (R2-11, 05/CONV-2)

This commit is contained in:
Joseph Doherty
2026-07-13 11:07:44 -04:00
parent 24435efa8b
commit b2a56ebf3b
3 changed files with 91 additions and 13 deletions
@@ -29,19 +29,44 @@ public sealed class ModbusDriverProbe : IDriverProbe
/// <inheritdoc />
public string DriverType => "Modbus";
/// <summary>The parsed probe target — host/port/unitId + the effective connection timeout, all read
/// from the SAME factory DTO shape (<c>ModbusDriverConfigDto</c>) the driver factory parses, so
/// <c>timeoutMs</c> binds identically to the factory (R2-11, 05/CONV-2; the OpcUaClient parity rule).</summary>
internal readonly record struct ProbeTarget(string Host, int Port, byte UnitId, TimeSpan Timeout);
/// <summary>Parse the driver config JSON into a <see cref="ProbeTarget"/> using the factory DTO shape.
/// Returns a null target + an error message when the JSON is invalid or has no host/port. The effective
/// timeout mirrors the factory (<c>TimeSpan.FromMilliseconds(dto.TimeoutMs ?? …)</c>); when the config
/// omits <c>timeoutMs</c> the caller's <paramref name="fallbackTimeout"/> is used.</summary>
/// <param name="configJson">The driver config JSON (factory DTO shape).</param>
/// <param name="fallbackTimeout">The timeout used when the config omits <c>timeoutMs</c>.</param>
/// <returns>The parsed target, or a null target with an error string.</returns>
internal static (ProbeTarget? Target, string? Error) TryParseProbeTarget(string configJson, TimeSpan fallbackTimeout)
{
ModbusDriverFactoryExtensions.ModbusDriverConfigDto? dto;
try { dto = JsonSerializer.Deserialize<ModbusDriverFactoryExtensions.ModbusDriverConfigDto>(configJson, _opts); }
catch (Exception ex) { return (null, $"Config JSON is invalid: {ex.Message}"); }
if (dto is null) return (null, "Config JSON deserialized to null.");
var port = dto.Port ?? 502;
if (string.IsNullOrWhiteSpace(dto.Host) || port <= 0)
return (null, "Config has no host/port to probe.");
var timeout = dto.TimeoutMs is { } ms && ms > 0 ? TimeSpan.FromMilliseconds(ms) : fallbackTimeout;
return (new ProbeTarget(dto.Host!, port, dto.UnitId ?? 1, timeout), null);
}
/// <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 (target, error) = TryParseProbeTarget(configJson, timeout);
if (target is not { } t) return new(false, error!, 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 host = t.Host;
var port = t.Port;
var unitId = t.UnitId;
// Honour the config's timeoutMs (factory parity) — the caller's timeout is the fallback.
timeout = t.Timeout;
var sw = Stopwatch.StartNew();
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
@@ -93,7 +118,4 @@ public sealed class ModbusDriverProbe : IDriverProbe
}
}
}
private static (string host, int port) ExtractTarget(ModbusDriverOptions opts)
=> (opts.Host, opts.Port);
}