namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus; /// /// Mitsubishi MELSEC PLC family selector for address-translation helpers. The Q/L/iQ-R /// families write bit-device addresses (X, Y) in hexadecimal in GX Works and the /// CPU manuals; the FX and iQ-F families write them in octal (same convention as /// AutomationDirect DirectLOGIC). Mixing the two up is the #1 MELSEC driver bug source — /// an operator typing X20 into a Q-series tag config means decimal 32, but the /// same string on an FX3U means decimal 16, so the helper must know the family to route /// correctly. /// public enum MelsecFamily { /// /// MELSEC-Q / MELSEC-L / MELSEC iQ-R. X and Y device numbers are interpreted as /// hexadecimal; X20 means decimal 32. /// Q_L_iQR, /// /// MELSEC-F (FX3U / FX3GE / FX3G) and MELSEC iQ-F (FX5U). X and Y device numbers /// are interpreted as octal (same as DirectLOGIC); X20 means decimal 16. /// iQ-F has a GX Works3 project toggle that can flip to decimal — if a site uses /// that, configure the tag's Address directly as a decimal PDU address and do not /// route through this helper. /// F_iQF, } /// /// Mitsubishi MELSEC address-translation helpers for the QJ71MT91 / LJ71MT91 / RJ71EN71 / /// iQ-R built-in / iQ-F / FX3U-ENET-P502 Modbus modules. MELSEC does NOT hard-wire /// Modbus-to-device mappings like DL260 does — every site configures its own "Modbus /// Device Assignment Parameter" block of up to 16 entries. The helpers here cover only /// the address-notation portion of the translation (hex X20 vs octal X20 + adding /// the bank base); the caller is still responsible for knowing the assignment-block /// offset for their site. /// /// /// See docs/v2/mitsubishi.md §device-assignment + §X-Y-hex-trap for the full /// matrix and primary-source citations. /// public static class MelsecAddress { /// /// Translate a MELSEC X-input address (e.g. "X0", "X10") to a 0-based /// Modbus discrete-input address, given the PLC family's address notation (hex or /// octal) and the Modbus Device Assignment block's X-range base. /// /// MELSEC X address. X prefix optional, case-insensitive. /// The PLC family — determines whether the trailing digits are hex or octal. /// /// 0-based Modbus DI address the assignment-block has configured X0 to land at. /// Typical default on QJ71MT91 sample projects: 0. Pass the site-specific value. /// public static ushort XInputToDiscrete(string xAddress, MelsecFamily family, ushort xBankBase = 0) => AddFamilyOffset(xBankBase, StripPrefix(xAddress, 'X'), family); /// /// Translate a MELSEC Y-output address to a 0-based Modbus coil address. Same rules /// as for hex/octal parsing. /// public static ushort YOutputToCoil(string yAddress, MelsecFamily family, ushort yBankBase = 0) => AddFamilyOffset(yBankBase, StripPrefix(yAddress, 'Y'), family); /// /// Translate a MELSEC M-relay address (internal relay) to a 0-based Modbus coil /// address. M-addresses are decimal on every MELSEC family — unlike X/Y which /// are hex on Q/L/iQ-R. Includes the bank base that the assignment-block configured. /// public static ushort MRelayToCoil(string mAddress, ushort mBankBase = 0) { var digits = StripPrefix(mAddress, 'M'); if (!ushort.TryParse(digits, out var offset)) throw new ArgumentException( $"M-relay address '{mAddress}' is not a valid decimal integer", nameof(mAddress)); var result = mBankBase + offset; if (result > ushort.MaxValue) throw new OverflowException($"M-relay {mAddress} + base {mBankBase} exceeds 0xFFFF"); return (ushort)result; } /// /// Translate a MELSEC D-register address (data register) to a 0-based Modbus holding /// register address. D-addresses are decimal. Default assignment convention is /// D0 → HR 0 (pass = 0); sites with shifted layouts pass /// their configured base. /// public static ushort DRegisterToHolding(string dAddress, ushort dBankBase = 0) { var digits = StripPrefix(dAddress, 'D'); if (!ushort.TryParse(digits, out var offset)) throw new ArgumentException( $"D-register address '{dAddress}' is not a valid decimal integer", nameof(dAddress)); var result = dBankBase + offset; if (result > ushort.MaxValue) throw new OverflowException($"D-register {dAddress} + base {dBankBase} exceeds 0xFFFF"); return (ushort)result; } 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); if (s.Length == 0) throw new ArgumentException($"Address '{address}' has no digits after prefix", nameof(address)); return s; } private static ushort AddFamilyOffset(ushort baseAddr, string digits, MelsecFamily family) { uint offset = family switch { MelsecFamily.Q_L_iQR => ParseHex(digits), MelsecFamily.F_iQF => ParseOctal(digits), _ => throw new ArgumentOutOfRangeException(nameof(family), family, "Unknown MELSEC family"), }; var result = baseAddr + offset; if (result > ushort.MaxValue) throw new OverflowException($"Address {baseAddr}+{offset} exceeds 0xFFFF"); return (ushort)result; } private static uint ParseHex(string digits) { uint result = 0; foreach (var ch in digits) { uint nibble; if (ch >= '0' && ch <= '9') nibble = (uint)(ch - '0'); else if (ch >= 'A' && ch <= 'F') nibble = (uint)(ch - 'A' + 10); else if (ch >= 'a' && ch <= 'f') nibble = (uint)(ch - 'a' + 10); else throw new ArgumentException( $"Address contains non-hex digit '{ch}' — Q/L/iQ-R X/Y addresses are hexadecimal", nameof(digits)); result = result * 16 + nibble; if (result > ushort.MaxValue) throw new OverflowException($"Hex address exceeds 0xFFFF"); } return result; } private static uint ParseOctal(string digits) { uint result = 0; foreach (var ch in digits) { if (ch < '0' || ch > '7') throw new ArgumentException( $"Address contains non-octal digit '{ch}' — FX/iQ-F X/Y addresses are octal (0-7)", nameof(digits)); result = result * 8 + (uint)(ch - '0'); if (result > ushort.MaxValue) throw new OverflowException($"Octal address exceeds 0xFFFF"); } return result; } }