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;
}
}