Task #139 — Modbus connection-layer config knobs (keep-alive / idle / reconnect)

Promotes the previously hardcoded transport-layer settings to ModbusDriverOptions
so users can tune them through DriverConfig JSON without recompiling.

Three new option groups:

1. KeepAlive (ModbusKeepAliveOptions): Enabled / Time / Interval / RetryCount.
   Defaults preserve the historical PR 53 wire output exactly (Enabled=true,
   Time=30s, Interval=10s, RetryCount=3). Set Enabled=false for PLCs that
   reject SO_KEEPALIVE.

2. IdleDisconnectTimeout (TimeSpan?): when set, the transport tracks last-PDU-
   success and proactively closes + reconnects on the next request after the
   threshold. Defends against silent NAT / firewall socket reaping. Default
   null = disabled (no behaviour change).

3. Reconnect (ModbusReconnectOptions): InitialDelay / MaxDelay /
   BackoffMultiplier for the post-drop reconnect loop. Defaults
   (InitialDelay=0, MaxDelay=30s, Multiplier=2.0) preserve the historical
   immediate-retry behaviour for the first attempt and add geometric backoff
   only if the reconnect itself fails. Capped at 10 attempts before propagating.

ModbusTcpTransport ctor extended with optional keepAlive / idleDisconnect /
reconnect parameters; existing 4-arg call sites continue to compile. Factory
DTO gains parallel KeepAlive / IdleDisconnectMs / Reconnect fields with
default-aware binding.

5 new ModbusConnectionOptionsTests covering the default-preservation contract
(every default field matches pre-#139) and the JSON-binding round-trip for
each knob group. Existing 204 unit tests still green.
This commit is contained in:
Joseph Doherty
2026-04-24 23:53:26 -04:00
parent 850b816873
commit 6cf20131fe
5 changed files with 264 additions and 12 deletions

View File

@@ -52,6 +52,20 @@ public static class ModbusDriverFactoryExtensions
Timeout = TimeSpan.FromMilliseconds(dto.Probe?.TimeoutMs ?? 2_000),
ProbeAddress = dto.Probe?.ProbeAddress ?? 0,
},
KeepAlive = dto.KeepAlive is null ? new ModbusKeepAliveOptions() : new ModbusKeepAliveOptions
{
Enabled = dto.KeepAlive.Enabled ?? true,
Time = TimeSpan.FromMilliseconds(dto.KeepAlive.TimeMs ?? 30_000),
Interval = TimeSpan.FromMilliseconds(dto.KeepAlive.IntervalMs ?? 10_000),
RetryCount = dto.KeepAlive.RetryCount ?? 3,
},
IdleDisconnectTimeout = dto.IdleDisconnectMs is { } ms ? TimeSpan.FromMilliseconds(ms) : null,
Reconnect = dto.Reconnect is null ? new ModbusReconnectOptions() : new ModbusReconnectOptions
{
InitialDelay = TimeSpan.FromMilliseconds(dto.Reconnect.InitialDelayMs ?? 0),
MaxDelay = TimeSpan.FromMilliseconds(dto.Reconnect.MaxDelayMs ?? 30_000),
BackoffMultiplier = dto.Reconnect.BackoffMultiplier ?? 2.0,
},
};
return new ModbusDriver(options, driverInstanceId);
@@ -136,6 +150,26 @@ public static class ModbusDriverFactoryExtensions
public bool? AutoReconnect { get; init; }
public List<ModbusTagDto>? Tags { get; init; }
public ModbusProbeDto? Probe { get; init; }
// #139 connection-layer knobs.
public ModbusKeepAliveDto? KeepAlive { get; init; }
public int? IdleDisconnectMs { get; init; }
public ModbusReconnectDto? Reconnect { get; init; }
}
internal sealed class ModbusKeepAliveDto
{
public bool? Enabled { get; init; }
public int? TimeMs { get; init; }
public int? IntervalMs { get; init; }
public int? RetryCount { get; init; }
}
internal sealed class ModbusReconnectDto
{
public int? InitialDelayMs { get; init; }
public int? MaxDelayMs { get; init; }
public double? BackoffMultiplier { get; init; }
}
internal sealed class ModbusTagDto