166 lines
7.7 KiB
C#
166 lines
7.7 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
|
|
|
/// <summary>
|
|
/// AutomationDirect DirectLOGIC address-translation helpers. DL205 / DL260 / DL350 CPUs
|
|
/// address V-memory in OCTAL while the Modbus wire uses DECIMAL PDU addresses — operators
|
|
/// see "V2000" in the PLC ladder-logic editor but the Modbus client must write PDU 0x0400.
|
|
/// The formulas differ between user V-memory (simple octal-to-decimal) and system V-memory
|
|
/// (fixed bank mappings), so the two cases are separate methods rather than one overloaded
|
|
/// "ToPdu" call.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// See <c>docs/v2/dl205.md</c> §V-memory for the full CPU-family matrix + rationale.
|
|
/// References: D2-USER-M appendix (DL205/D2-260), H2-ECOM-M §6.5 (absolute vs relative
|
|
/// addressing), AutomationDirect forum guidance on V40400 system-base.
|
|
/// </remarks>
|
|
public static class DirectLogicAddress
|
|
{
|
|
/// <summary>
|
|
/// Convert a DirectLOGIC user V-memory address (octal) to a 0-based Modbus PDU address.
|
|
/// Accepts bare octal (<c>"2000"</c>) or <c>V</c>-prefixed (<c>"V2000"</c>). Range
|
|
/// depends on CPU model — DL205 D2-260 user memory is V1400-V7377 + V10000-V17777
|
|
/// octal, DL260 extends to V77777 octal.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException">Input is null / empty / contains non-octal digits (8,9).</exception>
|
|
/// <exception cref="OverflowException">Parsed value exceeds ushort.MaxValue (0xFFFF).</exception>
|
|
public static ushort UserVMemoryToPdu(string vAddress)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(vAddress))
|
|
throw new ArgumentException("V-memory address must not be empty", nameof(vAddress));
|
|
var s = vAddress.Trim();
|
|
if (s[0] == 'V' || s[0] == 'v') s = s.Substring(1);
|
|
if (s.Length == 0)
|
|
throw new ArgumentException($"V-memory address '{vAddress}' has no digits", nameof(vAddress));
|
|
|
|
// Octal conversion. Reject 8/9 digits up-front — int.Parse in the obvious base would
|
|
// accept them silently because .NET has no built-in base-8 parser.
|
|
uint result = 0;
|
|
foreach (var ch in s)
|
|
{
|
|
if (ch < '0' || ch > '7')
|
|
throw new ArgumentException(
|
|
$"V-memory address '{vAddress}' contains non-octal digit '{ch}' — DirectLOGIC V-addresses are octal (0-7)",
|
|
nameof(vAddress));
|
|
result = result * 8 + (uint)(ch - '0');
|
|
if (result > ushort.MaxValue)
|
|
throw new OverflowException(
|
|
$"V-memory address '{vAddress}' exceeds the 16-bit Modbus PDU address range");
|
|
}
|
|
return (ushort)result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// DirectLOGIC system V-memory starts at octal V40400 on DL260 / H2-ECOM100 in factory
|
|
/// "absolute" addressing mode. Unlike user V-memory, the mapping is NOT a simple
|
|
/// octal-to-decimal conversion — the CPU relocates the system bank to Modbus PDU 0x2100
|
|
/// (decimal 8448). This helper returns the CPU-family base plus a user-supplied offset
|
|
/// within the system bank.
|
|
/// </summary>
|
|
public const ushort SystemVMemoryBasePdu = 0x2100;
|
|
|
|
/// <param name="offsetWithinSystemBank">
|
|
/// 0-based register offset within the system bank. Pass 0 for V40400 itself; pass 1 for
|
|
/// V40401 (octal), and so on. NOT an octal-decoded value — the system bank lives at
|
|
/// consecutive PDU addresses, so the offset is plain decimal.
|
|
/// </param>
|
|
public static ushort SystemVMemoryToPdu(ushort offsetWithinSystemBank)
|
|
{
|
|
var pdu = SystemVMemoryBasePdu + offsetWithinSystemBank;
|
|
if (pdu > ushort.MaxValue)
|
|
throw new OverflowException(
|
|
$"System V-memory offset {offsetWithinSystemBank} maps past 0xFFFF");
|
|
return (ushort)pdu;
|
|
}
|
|
|
|
// Bit-memory bases per DL260 user manual §I/O-configuration.
|
|
// Numbers after X / Y / C / SP are OCTAL in DirectLOGIC notation. The Modbus base is
|
|
// added to the octal-decoded offset; e.g. Y017 = Modbus coil 2048 + octal(17) = 2048 + 15 = 2063.
|
|
|
|
/// <summary>
|
|
/// DL260 Y-output coil base. Y0 octal → Modbus coil address 2048 (0-based).
|
|
/// </summary>
|
|
public const ushort YOutputBaseCoil = 2048;
|
|
|
|
/// <summary>
|
|
/// DL260 C-relay coil base. C0 octal → Modbus coil address 3072 (0-based).
|
|
/// </summary>
|
|
public const ushort CRelayBaseCoil = 3072;
|
|
|
|
/// <summary>
|
|
/// DL260 X-input discrete-input base. X0 octal → Modbus discrete input 0.
|
|
/// </summary>
|
|
public const ushort XInputBaseDiscrete = 0;
|
|
|
|
/// <summary>
|
|
/// DL260 SP special-relay discrete-input base. SP0 octal → Modbus discrete input 1024.
|
|
/// Read-only; writing SP relays is rejected with Illegal Data Address.
|
|
/// </summary>
|
|
public const ushort SpecialBaseDiscrete = 1024;
|
|
|
|
/// <summary>
|
|
/// Translate a DirectLOGIC Y-output address (e.g. <c>"Y0"</c>, <c>"Y17"</c>) to its
|
|
/// 0-based Modbus coil address on DL260. The trailing number is OCTAL, matching the
|
|
/// ladder-logic editor's notation.
|
|
/// </summary>
|
|
public static ushort YOutputToCoil(string yAddress) =>
|
|
AddOctalOffset(YOutputBaseCoil, StripPrefix(yAddress, 'Y'));
|
|
|
|
/// <summary>
|
|
/// Translate a DirectLOGIC C-relay address (e.g. <c>"C0"</c>, <c>"C1777"</c>) to its
|
|
/// 0-based Modbus coil address.
|
|
/// </summary>
|
|
public static ushort CRelayToCoil(string cAddress) =>
|
|
AddOctalOffset(CRelayBaseCoil, StripPrefix(cAddress, 'C'));
|
|
|
|
/// <summary>
|
|
/// Translate a DirectLOGIC X-input address (e.g. <c>"X0"</c>, <c>"X17"</c>) to its
|
|
/// 0-based Modbus discrete-input address. Reading an unpopulated X returns 0, not an
|
|
/// exception — the CPU sizes the table to configured I/O, not installed modules.
|
|
/// </summary>
|
|
public static ushort XInputToDiscrete(string xAddress) =>
|
|
AddOctalOffset(XInputBaseDiscrete, StripPrefix(xAddress, 'X'));
|
|
|
|
/// <summary>
|
|
/// Translate a DirectLOGIC SP-special-relay address (e.g. <c>"SP0"</c>) to its 0-based
|
|
/// Modbus discrete-input address. Accepts <c>"SP"</c> prefix case-insensitively.
|
|
/// </summary>
|
|
public static ushort SpecialToDiscrete(string spAddress)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(spAddress))
|
|
throw new ArgumentException("SP address must not be empty", nameof(spAddress));
|
|
var s = spAddress.Trim();
|
|
if (s.Length >= 2 && (s[0] == 'S' || s[0] == 's') && (s[1] == 'P' || s[1] == 'p'))
|
|
s = s.Substring(2);
|
|
return AddOctalOffset(SpecialBaseDiscrete, s);
|
|
}
|
|
|
|
private static string StripPrefix(string address, char expectedPrefix)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(address))
|
|
throw new ArgumentException("Address must not be empty", nameof(address));
|
|
var s = address.Trim();
|
|
if (s.Length > 0 && char.ToUpperInvariant(s[0]) == char.ToUpperInvariant(expectedPrefix))
|
|
s = s.Substring(1);
|
|
return s;
|
|
}
|
|
|
|
private static ushort AddOctalOffset(ushort baseAddr, string octalDigits)
|
|
{
|
|
if (octalDigits.Length == 0)
|
|
throw new ArgumentException("Address has no digits", nameof(octalDigits));
|
|
uint offset = 0;
|
|
foreach (var ch in octalDigits)
|
|
{
|
|
if (ch < '0' || ch > '7')
|
|
throw new ArgumentException(
|
|
$"Address contains non-octal digit '{ch}' — DirectLOGIC I/O addresses are octal (0-7)",
|
|
nameof(octalDigits));
|
|
offset = offset * 8 + (uint)(ch - '0');
|
|
}
|
|
var result = baseAddr + offset;
|
|
if (result > ushort.MaxValue)
|
|
throw new OverflowException($"Address {baseAddr}+{offset} exceeds 0xFFFF");
|
|
return (ushort)result;
|
|
}
|
|
}
|