162 lines
8.0 KiB
C#
162 lines
8.0 KiB
C#
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
|
|
{
|
|
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);
|
|
|
|
/// <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>
|
|
/// 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;
|
|
}
|
|
|
|
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);
|
|
/// <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>
|
|
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);
|
|
|
|
public enum ModbusRegion { Coils, DiscreteInputs, InputRegisters, HoldingRegisters }
|
|
|
|
public enum ModbusDataType
|
|
{
|
|
Bool,
|
|
Int16,
|
|
UInt16,
|
|
Int32,
|
|
UInt32,
|
|
Int64,
|
|
UInt64,
|
|
Float32,
|
|
Float64,
|
|
/// <summary>Single bit within a holding register. <see cref="ModbusTagDefinition.BitIndex"/> selects 0-15 LSB-first.</summary>
|
|
BitInRegister,
|
|
/// <summary>ASCII string packed 2 chars per register, <see cref="ModbusTagDefinition.StringLength"/> characters long.</summary>
|
|
String,
|
|
/// <summary>
|
|
/// 16-bit binary-coded decimal. Each nibble encodes one decimal digit (0-9). Register
|
|
/// value <c>0x1234</c> decodes as decimal <c>1234</c> — NOT binary <c>0x04D2 = 4660</c>.
|
|
/// DL205/DL260 and several Mitsubishi / Omron families store timers, counters, and
|
|
/// operator-facing numerics as BCD by default.
|
|
/// </summary>
|
|
Bcd16,
|
|
/// <summary>
|
|
/// 32-bit (two-register) BCD. Decodes 8 decimal digits. Word ordering follows
|
|
/// <see cref="ModbusTagDefinition.ByteOrder"/> the same way <see cref="Int32"/> does.
|
|
/// </summary>
|
|
Bcd32,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Word ordering for multi-register types. Modbus TCP standard is <see cref="BigEndian"/>
|
|
/// (ABCD for 32-bit: high word at the lower address). Many PLCs — Siemens S7, several
|
|
/// Allen-Bradley series, some Modicon families — use <see cref="WordSwap"/> (CDAB), which
|
|
/// keeps bytes big-endian within each register but reverses the word pair(s).
|
|
/// </summary>
|
|
public enum ModbusByteOrder
|
|
{
|
|
BigEndian,
|
|
WordSwap,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Per-register byte order for ASCII strings packed 2 chars per register. Standard Modbus
|
|
/// convention is <see cref="HighByteFirst"/> — 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 <see cref="LowByteFirst"/>, 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.
|
|
/// </summary>
|
|
public enum ModbusStringByteOrder
|
|
{
|
|
HighByteFirst,
|
|
LowByteFirst,
|
|
}
|