162 lines
7.5 KiB
C#
162 lines
7.5 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
|
|
|
/// <summary>
|
|
/// Mitsubishi MELSEC PLC family selector for address-translation helpers. The Q/L/iQ-R
|
|
/// families write bit-device addresses (X, Y) in <b>hexadecimal</b> in GX Works and the
|
|
/// CPU manuals; the FX and iQ-F families write them in <b>octal</b> (same convention as
|
|
/// AutomationDirect DirectLOGIC). Mixing the two up is the #1 MELSEC driver bug source —
|
|
/// an operator typing <c>X20</c> 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.
|
|
/// </summary>
|
|
public enum MelsecFamily
|
|
{
|
|
/// <summary>
|
|
/// MELSEC-Q / MELSEC-L / MELSEC iQ-R. X and Y device numbers are interpreted as
|
|
/// <b>hexadecimal</b>; <c>X20</c> means decimal 32.
|
|
/// </summary>
|
|
Q_L_iQR,
|
|
|
|
/// <summary>
|
|
/// MELSEC-F (FX3U / FX3GE / FX3G) and MELSEC iQ-F (FX5U). X and Y device numbers
|
|
/// are interpreted as <b>octal</b> (same as DirectLOGIC); <c>X20</c> 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.
|
|
/// </summary>
|
|
F_iQF,
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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 <b>address-notation</b> 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// See <c>docs/v2/mitsubishi.md</c> §device-assignment + §X-Y-hex-trap for the full
|
|
/// matrix and primary-source citations.
|
|
/// </remarks>
|
|
public static class MelsecAddress
|
|
{
|
|
/// <summary>
|
|
/// Translate a MELSEC X-input address (e.g. <c>"X0"</c>, <c>"X10"</c>) 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.
|
|
/// </summary>
|
|
/// <param name="xAddress">MELSEC X address. <c>X</c> prefix optional, case-insensitive.</param>
|
|
/// <param name="family">The PLC family — determines whether the trailing digits are hex or octal.</param>
|
|
/// <param name="xBankBase">
|
|
/// 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.
|
|
/// </param>
|
|
public static ushort XInputToDiscrete(string xAddress, MelsecFamily family, ushort xBankBase = 0) =>
|
|
AddFamilyOffset(xBankBase, StripPrefix(xAddress, 'X'), family);
|
|
|
|
/// <summary>
|
|
/// Translate a MELSEC Y-output address to a 0-based Modbus coil address. Same rules
|
|
/// as <see cref="XInputToDiscrete"/> for hex/octal parsing.
|
|
/// </summary>
|
|
public static ushort YOutputToCoil(string yAddress, MelsecFamily family, ushort yBankBase = 0) =>
|
|
AddFamilyOffset(yBankBase, StripPrefix(yAddress, 'Y'), family);
|
|
|
|
/// <summary>
|
|
/// Translate a MELSEC M-relay address (internal relay) to a 0-based Modbus coil
|
|
/// address. M-addresses are <b>decimal</b> on every MELSEC family — unlike X/Y which
|
|
/// are hex on Q/L/iQ-R. Includes the bank base that the assignment-block configured.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Translate a MELSEC D-register address (data register) to a 0-based Modbus holding
|
|
/// register address. D-addresses are <b>decimal</b>. Default assignment convention is
|
|
/// D0 → HR 0 (pass <paramref name="dBankBase"/> = 0); sites with shifted layouts pass
|
|
/// their configured base.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|