using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
///
/// Modbus TCP driver configuration. Bound from the driver's DriverConfig JSON at
/// DriverHost.RegisterAsync. Every register the driver exposes appears in
/// ; names become the OPC UA browse name + full reference.
///
public sealed class ModbusDriverOptions
{
public string Host { get; init; } = "127.0.0.1";
public int Port { get; init; } = 502;
public byte UnitId { get; init; } = 1;
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
/// Pre-declared tag map. Modbus has no discovery protocol — the driver returns exactly these.
public IReadOnlyList Tags { get; init; } = [];
///
/// Background connectivity-probe settings. When
/// is true the driver runs a tick loop that issues a cheap FC03 at register 0 every
/// and raises OnHostStatusChanged on
/// Running ↔ Stopped transitions. The Admin UI / OPC UA clients see the state through
/// .
///
public ModbusProbeOptions Probe { get; init; } = new();
///
/// 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 128, Mitsubishi Q/FX3U cap at 64,
/// Omron CJ/CS cap at 125. Set to the lowest cap across the devices this driver
/// instance talks to; the driver auto-chunks larger reads into consecutive requests.
/// Default 125 — the spec maximum, safe against any conforming server. Setting
/// to 0 disables the cap (discouraged — the spec upper bound still applies).
///
public ushort MaxRegistersPerRead { get; init; } = 125;
///
/// Maximum registers per FC16 (Write Multiple Registers) transaction. Spec maximum is
/// 123; DL205/DL260 cap at 100. 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 StringLength or splitting it into multiple tags).
///
public ushort MaxRegistersPerWrite { get; init; } = 123;
///
/// When true (default) the built-in detects
/// mid-transaction socket failures (,
/// ) 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.
///
public bool AutoReconnect { get; init; } = true;
}
public sealed class ModbusProbeOptions
{
public bool Enabled { get; init; } = true;
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
/// Register to read for the probe. Zero is usually safe; override for PLCs that lock register 0.
public ushort ProbeAddress { get; init; } = 0;
}
///
/// 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
/// field — real-world PLCs disagree on word ordering.
///
///
/// Tag name, used for both the OPC UA browse name and the driver's full reference. Must be
/// unique within the driver.
///
/// Coils / DiscreteInputs / InputRegisters / HoldingRegisters.
/// Zero-based address within the region.
///
/// Logical data type. See for the register count each encodes.
///
/// When true and Region supports writes (Coils / HoldingRegisters), IWritable routes writes here.
/// Word ordering for multi-register types. Ignored for Bool / Int16 / UInt16 / BitInRegister / String.
/// For DataType = BitInRegister: which bit of the holding register (0-15, LSB-first).
/// For DataType = String: number of ASCII characters (2 per register, rounded up).
///
/// Per-register byte order for DataType = String. Standard Modbus packs the first
/// character in the high byte ().
/// AutomationDirect DirectLOGIC (DL205/DL260) and a few legacy families pack the first
/// character in the low byte instead — see docs/v2/dl205.md §strings.
///
///
/// Per docs/v2/plan.md decisions #44, #45, #143 — flag a tag as safe to replay on
/// write timeout / failure. Default false; 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.
///
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);
public enum ModbusRegion { Coils, DiscreteInputs, InputRegisters, HoldingRegisters }
public enum ModbusDataType
{
Bool,
Int16,
UInt16,
Int32,
UInt32,
Int64,
UInt64,
Float32,
Float64,
/// Single bit within a holding register. selects 0-15 LSB-first.
BitInRegister,
/// ASCII string packed 2 chars per register, characters long.
String,
///
/// 16-bit binary-coded decimal. Each nibble encodes one decimal digit (0-9). Register
/// value 0x1234 decodes as decimal 1234 — NOT binary 0x04D2 = 4660.
/// DL205/DL260 and several Mitsubishi / Omron families store timers, counters, and
/// operator-facing numerics as BCD by default.
///
Bcd16,
///
/// 32-bit (two-register) BCD. Decodes 8 decimal digits. Word ordering follows
/// the same way does.
///
Bcd32,
}
///
/// Word ordering for multi-register types. Modbus TCP standard is
/// (ABCD for 32-bit: high word at the lower address). Many PLCs — Siemens S7, several
/// Allen-Bradley series, some Modicon families — use (CDAB), which
/// keeps bytes big-endian within each register but reverses the word pair(s).
///
public enum ModbusByteOrder
{
BigEndian,
WordSwap,
}
///
/// Per-register byte order for ASCII strings packed 2 chars per register. Standard Modbus
/// convention is — the first character of each pair occupies
/// the high byte of the register. AutomationDirect DirectLOGIC (DL205, DL260, DL350) and a
/// handful of legacy controllers pack , which inverts that within
/// each register. Word ordering across multiple registers is always ascending address for
/// strings — only the byte order inside each register flips.
///
public enum ModbusStringByteOrder
{
HighByteFirst,
LowByteFirst,
}