refactor(driver-modbus): extract ModbusDriverOptions to .Contracts
Move ModbusDriverOptions (and companion option types) to a new Driver.Modbus.Contracts sibling project. The contracts project references only Driver.Modbus.Addressing (itself zero-dep and Admin-safe) because ModbusDriverOptions.Probe/Family/Region properties use enum types that live there. Drop 'using ZB.MOM.WW.OtOpcUa.Core.Abstractions' and replace <see cref="IHostConnectivityProbe"/> with <c>IHostConnectivityProbe</c> per the approved decision — the using was doc-comment-only. The runtime Driver.Modbus project gains a ProjectReference back to .Contracts; the .slnx is updated accordingly.
This commit is contained in:
@@ -1,289 +0,0 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
||||
|
||||
/// <summary>
|
||||
/// Modbus TCP driver configuration. Bound from the driver's <c>DriverConfig</c> JSON at
|
||||
/// <c>DriverHost.RegisterAsync</c>. Every register the driver exposes appears in
|
||||
/// <see cref="Tags"/>; names become the OPC UA browse name + full reference.
|
||||
/// </summary>
|
||||
public sealed class ModbusDriverOptions
|
||||
{
|
||||
/// <summary>Gets the Modbus TCP server host address.</summary>
|
||||
public string Host { get; init; } = "127.0.0.1";
|
||||
/// <summary>Gets the Modbus TCP server port.</summary>
|
||||
public int Port { get; init; } = 502;
|
||||
/// <summary>Gets the Modbus unit ID (slave ID).</summary>
|
||||
public byte UnitId { get; init; } = 1;
|
||||
/// <summary>Gets the communication timeout duration.</summary>
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>Pre-declared tag map. Modbus has no discovery protocol — the driver returns exactly these.</summary>
|
||||
public IReadOnlyList<ModbusTagDefinition> Tags { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Background connectivity-probe settings. When <see cref="ModbusProbeOptions.Enabled"/>
|
||||
/// is true the driver runs a tick loop that issues a cheap FC03 at register 0 every
|
||||
/// <see cref="ModbusProbeOptions.Interval"/> and raises <c>OnHostStatusChanged</c> on
|
||||
/// Running ↔ Stopped transitions. The Admin UI / OPC UA clients see the state through
|
||||
/// <see cref="IHostConnectivityProbe"/>.
|
||||
/// </summary>
|
||||
public ModbusProbeOptions Probe { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Maximum registers per FC03 (Read Holding Registers) / FC04 (Read Input Registers)
|
||||
/// transaction. Modbus-TCP spec allows 125; many device families impose lower caps:
|
||||
/// AutomationDirect DL205/DL260 cap at <c>128</c>, Mitsubishi Q/FX3U cap at <c>64</c>,
|
||||
/// Omron CJ/CS cap at <c>125</c>. Set to the lowest cap across the devices this driver
|
||||
/// instance talks to; the driver auto-chunks larger reads into consecutive requests.
|
||||
/// Default <c>125</c> — the spec maximum, safe against any conforming server. Setting
|
||||
/// to <c>0</c> disables the cap (discouraged — the spec upper bound still applies).
|
||||
/// </summary>
|
||||
public ushort MaxRegistersPerRead { get; init; } = 125;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum registers per FC16 (Write Multiple Registers) transaction. Spec maximum is
|
||||
/// <c>123</c>; DL205/DL260 cap at <c>100</c>. Matching caller-vs-device semantics:
|
||||
/// exceeding the cap currently throws (writes aren't auto-chunked because a partial
|
||||
/// write across two FC16 calls is no longer atomic — caller must explicitly opt in
|
||||
/// by shortening the tag's <c>StringLength</c> or splitting it into multiple tags).
|
||||
/// </summary>
|
||||
public ushort MaxRegistersPerWrite { get; init; } = 123;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum coils per FC01 (Read Coils) / FC02 (Read Discrete Inputs) transaction. Modbus
|
||||
/// spec allows up to <c>2000</c> bits per request — separate from
|
||||
/// <see cref="MaxRegistersPerRead"/> because the underlying packing is different
|
||||
/// (1 bit per coil vs 16 bits per register). Default <c>2000</c>; setting to <c>0</c>
|
||||
/// disables the cap. The driver auto-chunks coil-array reads above the cap.
|
||||
/// </summary>
|
||||
public ushort MaxCoilsPerRead { get; init; } = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, single-element coil writes use FC15 (Write Multiple Coils) with
|
||||
/// <c>quantity=1</c> instead of the default FC05 (Write Single Coil). Safety / audit
|
||||
/// PLCs that only accept the multi-write function codes need this. Default <c>false</c>
|
||||
/// preserves the existing FC05 path.
|
||||
/// </summary>
|
||||
public bool UseFC15ForSingleCoilWrites { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, single-element holding-register writes use FC16 (Write Multiple
|
||||
/// Registers) with <c>quantity=1</c> instead of the default FC06 (Write Single
|
||||
/// Register). Same use-case as <see cref="UseFC15ForSingleCoilWrites"/>. Default
|
||||
/// <c>false</c>.
|
||||
/// </summary>
|
||||
public bool UseFC16ForSingleRegisterWrites { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// <b>Reserved / no-op</b> kill-switch for FC23 (Read/Write Multiple Registers). The
|
||||
/// driver does not currently emit FC23 — toggling this option has no observable effect
|
||||
/// today. The slot exists so a future block-read-coalescing enhancement that opts into
|
||||
/// FC23 can be disabled per-deployment without a code change. Track Driver.Modbus-007
|
||||
/// for the wiring follow-up. Default <c>false</c>.
|
||||
/// </summary>
|
||||
public bool DisableFC23 { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// #151 — interval for the background re-probe loop that retries auto-prohibited
|
||||
/// coalesced ranges (#148). When non-null, every <c>AutoProhibitReprobeInterval</c>
|
||||
/// the driver attempts each prohibition's coalesced read once. If the re-probe
|
||||
/// succeeds, the prohibition clears and the planner resumes coalescing across the
|
||||
/// range on the next scan. Default <c>null</c> = re-probe disabled (prohibitions
|
||||
/// persist until <c>ReinitializeAsync</c>; preserves pre-#151 behaviour).
|
||||
/// </summary>
|
||||
public TimeSpan? AutoProhibitReprobeInterval { get; init; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Block-read coalescing budget (#143). When non-zero, the read planner combines tags
|
||||
/// in the same (UnitId, Region) group whose addresses are at most this many registers
|
||||
/// apart into a single FC03/FC04/FC01/FC02 read. The sliced response is then dispatched
|
||||
/// back to per-tag values. Default <c>0</c> = no coalescing — every tag gets its own
|
||||
/// PDU (preserves pre-#143 behaviour). Typical opt-in values are 5..32 — large enough
|
||||
/// to bridge a few unused registers, small enough to avoid trampling protected holes.
|
||||
/// </summary>
|
||||
public ushort MaxReadGap { get; init; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// PLC family hint that drives the parser's family-native branch (#144). When set to a
|
||||
/// non-Generic value, address strings using that family's native syntax (DL205 V2000 /
|
||||
/// MELSEC D100) parse to the right region + offset directly. Defaults to
|
||||
/// <see cref="ModbusFamily.Generic"/> = Modicon-only behaviour preserved from #137.
|
||||
/// </summary>
|
||||
public ModbusFamily Family { get; init; } = ModbusFamily.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// MELSEC sub-family selector — only consulted when <see cref="Family"/> = MELSEC.
|
||||
/// Default Q/L/iQR (hex X/Y interpretation). Set F_iQF for FX-series PLCs.
|
||||
/// </summary>
|
||||
public MelsecFamily MelsecSubFamily { get; init; } = MelsecFamily.Q_L_iQR;
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, the driver suppresses redundant writes: if the most recent
|
||||
/// successful write to a tag carried value V and a new write of V arrives, the second
|
||||
/// write returns Good without touching the wire. Saves PLC bandwidth on clients that
|
||||
/// re-publish the same setpoint every scan. The cached "last written" is invalidated
|
||||
/// on the next read that returns a different value, so HMI-side changes don't get
|
||||
/// masked. Default <c>false</c> preserves the historical "every write goes to the wire"
|
||||
/// behaviour. Per-tag deadband lives on <c>ModbusTagDefinition.Deadband</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Driver.Modbus-010 — write-only-tag caveat:</b> the suppression cache is only
|
||||
/// invalidated by a <i>read</i> that returns a divergent value. A tag that is never
|
||||
/// subscribed or polled (write-only setpoints, command registers) never sees its
|
||||
/// cache entry refreshed — so a value the operator believes was re-asserted is
|
||||
/// silently suppressed forever after the first write. There is no time- or
|
||||
/// count-based expiry. If you set <see cref="WriteOnChangeOnly"/> = <c>true</c>,
|
||||
/// either subscribe / poll every tag that needs deterministic re-write, or leave
|
||||
/// this option <c>false</c> for the affected driver instance.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool WriteOnChangeOnly { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c> (default) the built-in <see cref="ModbusTcpTransport"/> detects
|
||||
/// mid-transaction socket failures (<see cref="System.IO.EndOfStreamException"/>,
|
||||
/// <see cref="System.Net.Sockets.SocketException"/>) and transparently reconnects +
|
||||
/// retries the PDU exactly once. Required for DL205/DL260 because the H2-ECOM100
|
||||
/// does not send TCP keepalives — intermediate NAT / firewall devices silently close
|
||||
/// idle sockets and the first send after the drop would otherwise surface as a
|
||||
/// connection error to the caller even though the PLC is up.
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Per-driver TCP keep-alive settings. Defaults are the historical PR 53 values
|
||||
/// (KeepAliveEnabled=true, Time=30s, Interval=10s, RetryCount=3) so existing
|
||||
/// deployments see no behaviour change. Set <see cref="ModbusKeepAliveOptions.Enabled"/>
|
||||
/// to <c>false</c> to disable OS-level keep-alive entirely (some PLCs don't tolerate it).
|
||||
/// </summary>
|
||||
public ModbusKeepAliveOptions KeepAlive { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// If non-null, the transport tracks the time of the last successful PDU and proactively
|
||||
/// closes + reconnects the socket on the next request after this idle threshold elapses.
|
||||
/// Defends against silent NAT / firewall reaping of long-idle sockets — the explicit
|
||||
/// close-and-reopen turns the failure mode from "first-send-after-X-minutes errors" into
|
||||
/// "first-send-after-X-minutes pays one reconnect cost."
|
||||
/// </summary>
|
||||
public TimeSpan? IdleDisconnectTimeout { get; init; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Reconnect backoff settings used by the auto-reconnect path. Default is no backoff
|
||||
/// (immediate retry — preserves the historical pre-#139 behaviour). Set to a non-zero
|
||||
/// <see cref="ModbusReconnectOptions.InitialDelay"/> to sleep before the first reconnect
|
||||
/// attempt; <see cref="ModbusReconnectOptions.MaxDelay"/> caps the geometric growth.
|
||||
/// </summary>
|
||||
public ModbusReconnectOptions Reconnect { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>OS-level TCP keep-alive knobs. Set <see cref="Enabled"/>=false to skip entirely.</summary>
|
||||
public sealed class ModbusKeepAliveOptions
|
||||
{
|
||||
/// <summary>Gets a value indicating whether keep-alive is enabled.</summary>
|
||||
public bool Enabled { get; init; } = true;
|
||||
/// <summary>Idle time before the first probe (seconds, mapped to <c>TcpKeepAliveTime</c>).</summary>
|
||||
public TimeSpan Time { get; init; } = TimeSpan.FromSeconds(30);
|
||||
/// <summary>Interval between probes once started (seconds, mapped to <c>TcpKeepAliveInterval</c>).</summary>
|
||||
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(10);
|
||||
/// <summary>Probes before declaring the socket dead (mapped to <c>TcpKeepAliveRetryCount</c>).</summary>
|
||||
public int RetryCount { get; init; } = 3;
|
||||
}
|
||||
|
||||
/// <summary>Geometric-backoff settings for the post-drop reconnect loop.</summary>
|
||||
public sealed class ModbusReconnectOptions
|
||||
{
|
||||
/// <summary>Delay before the first reconnect attempt. Default zero = immediate.</summary>
|
||||
public TimeSpan InitialDelay { get; init; } = TimeSpan.Zero;
|
||||
/// <summary>Upper bound on the geometric backoff sequence.</summary>
|
||||
public TimeSpan MaxDelay { get; init; } = TimeSpan.FromSeconds(30);
|
||||
/// <summary>Multiplier applied each retry. Default 2.0 doubles each step.</summary>
|
||||
public double BackoffMultiplier { get; init; } = 2.0;
|
||||
}
|
||||
|
||||
public sealed class ModbusProbeOptions
|
||||
{
|
||||
/// <summary>Gets a value indicating whether probing is enabled.</summary>
|
||||
public bool Enabled { get; init; } = true;
|
||||
/// <summary>Gets the interval between probe requests.</summary>
|
||||
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
|
||||
/// <summary>Gets the probe request timeout.</summary>
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
|
||||
/// <summary>Register to read for the probe. Zero is usually safe; override for PLCs that lock register 0.</summary>
|
||||
public ushort ProbeAddress { get; init; } = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One Modbus-backed OPC UA variable. Address is zero-based (Modbus spec numbering, not
|
||||
/// the documentation's 1-based coil/register conventions). Multi-register types
|
||||
/// (Int32/UInt32/Float32 = 2 regs; Int64/UInt64/Float64 = 4 regs) respect the
|
||||
/// <see cref="ByteOrder"/> field — real-world PLCs disagree on word ordering.
|
||||
/// </summary>
|
||||
/// <param name="Name">
|
||||
/// Tag name, used for both the OPC UA browse name and the driver's full reference. Must be
|
||||
/// unique within the driver.
|
||||
/// </param>
|
||||
/// <param name="Region">Coils / DiscreteInputs / InputRegisters / HoldingRegisters.</param>
|
||||
/// <param name="Address">Zero-based address within the region.</param>
|
||||
/// <param name="DataType">
|
||||
/// Logical data type. See <see cref="ModbusDataType"/> for the register count each encodes.
|
||||
/// </param>
|
||||
/// <param name="Writable">When true and Region supports writes (Coils / HoldingRegisters), IWritable routes writes here.</param>
|
||||
/// <param name="ByteOrder">Word ordering for multi-register types. Ignored for Bool / Int16 / UInt16 / BitInRegister / String.</param>
|
||||
/// <param name="BitIndex">For <c>DataType = BitInRegister</c>: which bit of the holding register (0-15, LSB-first).</param>
|
||||
/// <param name="StringLength">For <c>DataType = String</c>: number of ASCII characters (2 per register, rounded up).</param>
|
||||
/// <param name="StringByteOrder">
|
||||
/// Per-register byte order for <c>DataType = String</c>. Standard Modbus packs the first
|
||||
/// character in the high byte (<see cref="ModbusStringByteOrder.HighByteFirst"/>).
|
||||
/// AutomationDirect DirectLOGIC (DL205/DL260) and a few legacy families pack the first
|
||||
/// character in the low byte instead — see <c>docs/v2/dl205.md</c> §strings.
|
||||
/// </param>
|
||||
/// <param name="WriteIdempotent">
|
||||
/// Per <c>docs/v2/plan.md</c> decisions #44, #45, #143 — flag a tag as safe to replay on
|
||||
/// write timeout / failure. Default <c>false</c>; writes do not auto-retry. Safe candidates:
|
||||
/// holding-register set-points for analog values and configuration registers where the same
|
||||
/// value can be written again without side-effects. Unsafe: coils that drive edge-triggered
|
||||
/// actions (pulse outputs), counter-increment addresses on PLCs that treat writes as deltas,
|
||||
/// any BCD / counter register where repeat-writes advance state.
|
||||
/// </param>
|
||||
/// <param name="ArrayCount">
|
||||
/// When non-null, the tag is exposed as an OPC UA array of this many elements. Total
|
||||
/// registers consumed = ArrayCount * registers-per-element. Bit + array is rejected at
|
||||
/// bind time (no use case). Default null = scalar (existing behavior).
|
||||
/// </param>
|
||||
/// <param name="Deadband">
|
||||
/// When non-null, the subscribe path suppresses a publish whenever
|
||||
/// <c>|new - last_published| < Deadband</c>. Reduces wire traffic on noisy analog
|
||||
/// signals (flow meters, temperatures). Only meaningful for numeric scalar types
|
||||
/// (Int*, UInt*, Float32, Float64, Bcd*); ignored for Bool / BitInRegister / String /
|
||||
/// array tags. Default null = no deadband (every change publishes).
|
||||
/// </param>
|
||||
/// <param name="UnitId">
|
||||
/// Per-tag UnitId override for multi-slave gateway topology (#142). When non-null this
|
||||
/// UnitId is used in the MBAP header instead of the driver-level <c>ModbusDriverOptions.UnitId</c>.
|
||||
/// Defaults to null = use the driver-level value (preserves single-slave deployments).
|
||||
/// Tags with different UnitIds belong to different physical slaves and the read planner
|
||||
/// must NOT coalesce them across slaves — even at the same address.
|
||||
/// </param>
|
||||
/// <param name="CoalesceProhibited">
|
||||
/// Escape hatch for #143 block-read coalescing. When <c>true</c>, the planner reads this
|
||||
/// tag in isolation regardless of <c>ModbusDriverOptions.MaxReadGap</c>. Use when the
|
||||
/// surrounding registers are write-only or fault on read (some Schneider Premium / Siemens
|
||||
/// PNs have protected holes). Default <c>false</c>.
|
||||
/// </param>
|
||||
public sealed record ModbusTagDefinition(
|
||||
string Name,
|
||||
ModbusRegion Region,
|
||||
ushort Address,
|
||||
ModbusDataType DataType,
|
||||
bool Writable = true,
|
||||
ModbusByteOrder ByteOrder = ModbusByteOrder.BigEndian,
|
||||
byte BitIndex = 0,
|
||||
ushort StringLength = 0,
|
||||
ModbusStringByteOrder StringByteOrder = ModbusStringByteOrder.HighByteFirst,
|
||||
bool WriteIdempotent = false,
|
||||
int? ArrayCount = null,
|
||||
double? Deadband = null,
|
||||
byte? UnitId = null,
|
||||
bool CoalesceProhibited = false);
|
||||
@@ -15,6 +15,7 @@
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core\ZB.MOM.WW.OtOpcUa.Core.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing\ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts\ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user