namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus; /// /// 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. /// /// /// See docs/v2/dl205.md §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. /// public static class DirectLogicAddress { /// /// Convert a DirectLOGIC user V-memory address (octal) to a 0-based Modbus PDU address. /// Accepts bare octal ("2000") or V-prefixed ("V2000"). Range /// depends on CPU model — DL205 D2-260 user memory is V1400-V7377 + V10000-V17777 /// octal, DL260 extends to V77777 octal. /// /// Input is null / empty / contains non-octal digits (8,9). /// Parsed value exceeds ushort.MaxValue (0xFFFF). 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; } /// /// 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. /// public const ushort SystemVMemoryBasePdu = 0x2100; /// /// 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. /// 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. /// /// DL260 Y-output coil base. Y0 octal → Modbus coil address 2048 (0-based). /// public const ushort YOutputBaseCoil = 2048; /// /// DL260 C-relay coil base. C0 octal → Modbus coil address 3072 (0-based). /// public const ushort CRelayBaseCoil = 3072; /// /// DL260 X-input discrete-input base. X0 octal → Modbus discrete input 0. /// public const ushort XInputBaseDiscrete = 0; /// /// DL260 SP special-relay discrete-input base. SP0 octal → Modbus discrete input 1024. /// Read-only; writing SP relays is rejected with Illegal Data Address. /// public const ushort SpecialBaseDiscrete = 1024; /// /// Translate a DirectLOGIC Y-output address (e.g. "Y0", "Y17") to its /// 0-based Modbus coil address on DL260. The trailing number is OCTAL, matching the /// ladder-logic editor's notation. /// public static ushort YOutputToCoil(string yAddress) => AddOctalOffset(YOutputBaseCoil, StripPrefix(yAddress, 'Y')); /// /// Translate a DirectLOGIC C-relay address (e.g. "C0", "C1777") to its /// 0-based Modbus coil address. /// public static ushort CRelayToCoil(string cAddress) => AddOctalOffset(CRelayBaseCoil, StripPrefix(cAddress, 'C')); /// /// Translate a DirectLOGIC X-input address (e.g. "X0", "X17") 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. /// public static ushort XInputToDiscrete(string xAddress) => AddOctalOffset(XInputBaseDiscrete, StripPrefix(xAddress, 'X')); /// /// Translate a DirectLOGIC SP-special-relay address (e.g. "SP0") to its 0-based /// Modbus discrete-input address. Accepts "SP" prefix case-insensitively. /// 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; } }